From 4bca9ac31f6e528b1935231213128ef372dde4ed Mon Sep 17 00:00:00 2001 From: morganick Date: Mon, 13 May 2024 13:59:28 +0000 Subject: [PATCH] deploy: 6f7c3de6c9d6e44172f9dfdd0850b45d453dcaaf --- 404.html | 4 +- assets/js/145425bc.d974fddb.js | 1 + assets/js/145425bc.e78ce3c0.js | 1 - assets/js/47c232e1.403201ca.js | 1 + assets/js/52d269c5.88ca394b.js | 1 + assets/js/55960ee5.67bbd847.js | 1 + assets/js/55960ee5.d6f1a6f9.js | 1 - assets/js/6558e733.09ef56bf.js | 1 + assets/js/6c727604.37636773.js | 1 + assets/js/935f2afb.1e97105a.js | 1 + assets/js/935f2afb.a3a52f9d.js | 1 - assets/js/b747e1af.980c6b16.js | 1 + assets/js/ecce3b64.40438a91.js | 1 + assets/js/ecce3b64.a27cf30f.js | 1 - assets/js/fe9b09bf.18b32ed9.js | 1 + assets/js/fe9b09bf.7b3890df.js | 1 - assets/js/main.67a44e7b.js | 2 - assets/js/main.973d3360.js | 2 + ...CENSE.txt => main.973d3360.js.LICENSE.txt} | 0 assets/js/runtime~main.5c217811.js | 1 + assets/js/runtime~main.bfee932e.js | 1 - docs/archive/PristineExpoProject/index.html | 6 +- docs/archive/index.html | 6 +- docs/intro/index.html | 6 +- .../recipes/AccessibilityFontSizes/index.html | 8 +- docs/recipes/Authentication/index.html | 170 ++ docs/recipes/CircleCIRNSetup/index.html | 8 +- .../index.html | 6 +- docs/recipes/DetoxIntro/index.html | 6 +- .../DistributingAuthTokenToAPI/index.html | 6 +- docs/recipes/EASUpdate/index.html | 6 +- docs/recipes/EnforcingImportOrder/index.html | 6 +- docs/recipes/EnvironmentVariables/index.html | 6 +- docs/recipes/ExpoRouter/index.html | 6 +- .../GeneratorComponentTests/index.html | 6 +- .../LocalFirstDataWithPowerSync/index.html | 6 +- docs/recipes/MaestroSetup/index.html | 6 +- docs/recipes/MigratingToMMKV/index.html | 6 +- .../PatchingBuildingAndroid/index.html | 6 +- docs/recipes/PrepForEASBuild/index.html | 6 +- .../ReactNativeVisionCamera/index.html | 6 +- docs/recipes/Redux/index.html | 6 +- docs/recipes/RemoveMobxStateTree/index.html | 6 +- .../index.html | 6 +- docs/recipes/SampleYAMLCircleCI/index.html | 6 +- .../SelectFieldWithBottomSheet/index.html | 6 +- .../recipes/SwitchBetweenExpoGoCNG/index.html | 6 +- docs/recipes/TypeScriptBaseURL/index.html | 6 +- .../UnrenderedItemInScrollView/index.html | 6 +- docs/recipes/UpdatingDependencies/index.html | 6 +- docs/recipes/UpdatingIgnite/index.html | 6 +- docs/recipes/UsingScreenReaders/index.html | 6 +- docs/recipes/Zustand/index.html | 6 +- docs/tags/accessibility/index.html | 4 +- docs/tags/android/index.html | 4 +- docs/tags/apisauce/index.html | 4 +- docs/tags/archive/index.html | 4 +- docs/tags/async-storage/index.html | 4 +- docs/tags/authentication/index.html | 8 +- docs/tags/babel/index.html | 4 +- docs/tags/backend/index.html | 4 +- docs/tags/ci-cd/index.html | 4 +- docs/tags/cng/index.html | 4 +- docs/tags/data-synchronization/index.html | 4 +- docs/tags/database/index.html | 4 +- docs/tags/debug/index.html | 4 +- docs/tags/dependencies/index.html | 4 +- docs/tags/eas-update/index.html | 4 +- docs/tags/eas/index.html | 4 +- docs/tags/environment-variables/index.html | 4 +- docs/tags/expo-dev-client/index.html | 4 +- docs/tags/expo-router/index.html | 4 +- docs/tags/expo-updates/index.html | 4 +- docs/tags/expo/index.html | 4 +- docs/tags/flat-list/index.html | 4 +- docs/tags/generator/index.html | 4 +- docs/tags/guide/index.html | 4 +- docs/tags/hardware/index.html | 4 +- docs/tags/i-os/index.html | 4 +- docs/tags/imports/index.html | 4 +- docs/tags/index.html | 6 +- docs/tags/intro/index.html | 4 +- docs/tags/login/index.html | 41 + docs/tags/maestro/index.html | 4 +- docs/tags/mmkv/index.html | 4 +- docs/tags/mob-x/index.html | 4 +- docs/tags/offline-support/index.html | 4 +- docs/tags/power-sync/index.html | 4 +- docs/tags/prebuild/index.html | 4 +- docs/tags/prettier/index.html | 4 +- .../react-native-vision-camera/index.html | 4 +- docs/tags/react-native/index.html | 4 +- docs/tags/react-navigation/index.html | 4 +- docs/tags/redux/index.html | 4 +- docs/tags/scroll-to/index.html | 4 +- docs/tags/section-list/index.html | 4 +- docs/tags/select-field/index.html | 4 +- docs/tags/session/index.html | 41 + docs/tags/signup/index.html | 41 + docs/tags/state-management/index.html | 4 +- docs/tags/supabase/index.html | 41 + docs/tags/testing/index.html | 4 +- docs/tags/text-field/index.html | 4 +- docs/tags/type-script/index.html | 4 +- .../index.html | 4 +- docs/tags/ui/index.html | 4 +- docs/tags/uses-feature/index.html | 4 +- docs/tags/vision-camera/index.html | 4 +- docs/tags/yarn/index.html | 4 +- docs/tags/zustand/index.html | 4 +- index.html | 2296 +++++++---------- search/index.html | 4 +- sitemap.xml | 2 +- 113 files changed, 1550 insertions(+), 1519 deletions(-) create mode 100644 assets/js/145425bc.d974fddb.js delete mode 100644 assets/js/145425bc.e78ce3c0.js create mode 100644 assets/js/47c232e1.403201ca.js create mode 100644 assets/js/52d269c5.88ca394b.js create mode 100644 assets/js/55960ee5.67bbd847.js delete mode 100644 assets/js/55960ee5.d6f1a6f9.js create mode 100644 assets/js/6558e733.09ef56bf.js create mode 100644 assets/js/6c727604.37636773.js create mode 100644 assets/js/935f2afb.1e97105a.js delete mode 100644 assets/js/935f2afb.a3a52f9d.js create mode 100644 assets/js/b747e1af.980c6b16.js create mode 100644 assets/js/ecce3b64.40438a91.js delete mode 100644 assets/js/ecce3b64.a27cf30f.js create mode 100644 assets/js/fe9b09bf.18b32ed9.js delete mode 100644 assets/js/fe9b09bf.7b3890df.js delete mode 100644 assets/js/main.67a44e7b.js create mode 100644 assets/js/main.973d3360.js rename assets/js/{main.67a44e7b.js.LICENSE.txt => main.973d3360.js.LICENSE.txt} (100%) create mode 100644 assets/js/runtime~main.5c217811.js delete mode 100644 assets/js/runtime~main.bfee932e.js create mode 100644 docs/recipes/Authentication/index.html create mode 100644 docs/tags/login/index.html create mode 100644 docs/tags/session/index.html create mode 100644 docs/tags/signup/index.html create mode 100644 docs/tags/supabase/index.html diff --git a/404.html b/404.html index 5294afd9..9e82e9a9 100644 --- a/404.html +++ b/404.html @@ -10,8 +10,8 @@ - - + +
Skip to main content
warning

This recipe is for Ignite v8 only.

+
warning

This recipe is for Ignite v8 only.

Pristine Expo Project

Ignite sets your project up ready to run both a bare React Native project or with Expo.

However, if you don't want to manage any of the native files going forward, you can follow these steps to get to an Expo only project structure.

diff --git a/docs/archive/index.html b/docs/archive/index.html index a5f2f253..33c9cd8c 100644 --- a/docs/archive/index.html +++ b/docs/archive/index.html @@ -10,8 +10,8 @@ - - + +
+
\ No newline at end of file diff --git a/docs/intro/index.html b/docs/intro/index.html index 765e1380..0f2b9b96 100644 --- a/docs/intro/index.html +++ b/docs/intro/index.html @@ -10,8 +10,8 @@ - - + +

Welcome to the Ignite Cookbook 👋

+

Welcome to the Ignite Cookbook 👋

This is a collection of recipes for common patterns that we use sometimes at Infinite Red or in the Ignite community but don't quite belong in the Ignite boilerplate directly. We'll be adding to this over time, so be sure to check back often!

What is a Recipe?

A recipe is a collection of steps that you can follow to accomplish a common task or pattern in your Ignite project. Recipes are written to be as simple as possible, and are meant to be a starting point for you to build upon. You can use them as-is, or you can use them as a reference to help you create your own solutions.

diff --git a/docs/recipes/AccessibilityFontSizes/index.html b/docs/recipes/AccessibilityFontSizes/index.html index 4fb62cd6..352a9203 100644 --- a/docs/recipes/AccessibilityFontSizes/index.html +++ b/docs/recipes/AccessibilityFontSizes/index.html @@ -10,8 +10,8 @@ - - + +

Dealing With Accessibility Font Sizes in React Native

+

Dealing With Accessibility Font Sizes in React Native

Modern phones have a lot of accessibility options. Users can make the font size on Android GIGANTIC. This is a way you can allow users to scale their fonts larger and smaller, but only to a certain point. We wanted the accessibility but not the extreme ends of it, just to keep things readable without turning off font scaling completely.

-
import * as React from "react";
import { View, TextProps, PixelRatio, AppState } from "react-native";
import { MaterialTopTabNavigationOptions } from "@react-navigation/material-top-tabs";
import { StackNavigationOptions } from "@react-navigation/stack";
import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs";
import { DrawerNavigationOptions } from "@react-navigation/drawer";
import { Text } from "./Text";

// These constants determine how much bigger the font size should get based on the user's
// accessibility settings. Even if they turn the dial all the way to 11, we will only ever
// scale the fonts by these factors. This is to prevent the font size from getting too large
// and completely breaking the layout.
const MAX_FONT_SCALE = 1.2;
const MIN_FONT_SCALE = 0.8;

// Returns fontScaling props for Text and TextInput components
// Usage:
// const fontProps = useFontScaling();
// return <Text {...fontProps}>Text Here</Text>;
export const useFontScaling = (): Partial<TextProps> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<TextProps> = React.useMemo(() => {
return {
minimumFontScale: allowFontScaling ? MIN_FONT_SCALE : 1, // This prevents the font from getting too small.
maxFontSizeMultiplier: allowFontScaling ? MAX_FONT_SCALE : 1, // This prevents the font from getting too big.
allowFontScaling, // This allows the font to be scaled or not.
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Navigator components
export const useNavigatorFontScalingScreenOptions =
(): Partial<StackNavigationOptions> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<StackNavigationOptions> = React.useMemo(() => {
return {
headerBackAllowFontScaling: allowFontScaling,
headerTitleAllowFontScaling: allowFontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Top Tab Navigator components
export const useTopTabNavigatorFontScalingScreenOptions =
(): Partial<MaterialTopTabNavigationOptions> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<MaterialTopTabNavigationOptions> =
React.useMemo(() => {
return {
tabBarAllowFontScaling: allowFontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Tab Navigator components
export const useTabNavigatorFontScalingScreenOptions =
(): Partial<BottomTabNavigationOptions> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<BottomTabNavigationOptions> =
React.useMemo(() => {
return {
tabBarAllowFontScaling: fontScaling,
headerTitleAllowFontScaling: fontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Tab Navigator components
export const useDrawerNavigatorFontScalingScreenOptions =
(): Partial<DrawerNavigationOptions> => {
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<DrawerNavigationOptions> = React.useMemo(() => {
return {
drawerAllowFontScaling: allowFontScaling,
headerTitleAllowFontScaling: allowFontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Use this handy __DEV__ mode only component to figure out what the font size is actually doing.
export const DevFontSize = () => {
const [allowFontScaling,] = React.useState(true);
const [appStateVisible, setAppStateVisible] = React.useState(
AppState.currentState
);

React.useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
setAppStateVisible(nextAppState);
});

return () => subscription.remove();
}, []);

// This memo has to listen to appStateVisible even though it's not a direct dependency
// so that we can reload the font size when the app switches back from user settings.
const fontSize = React.useMemo(() => {
if (allowFontScaling) {
return Math.min(
Math.max(PixelRatio.getFontScale(), MIN_FONT_SCALE),
MAX_FONT_SCALE
);
} else {
return 1.0;
}
}, [allowFontScaling, appStateVisible]); // eslint-disable-line react-hooks/exhaustive-deps

return __DEV__ ? (
<View style={{
backgroundColor: '#E58F83',
padding: 10,
margin: 10,
borderRadius: 5,
borderColor: '#000,
borderWidth: 1,
}}>
<Text>
User Font Setting: {Math.trunc(PixelRatio.getFontScale() * 100) / 100}
</Text>
<Text>
Currently limiting ratio to: {Math.trunc(fontSize * 100) / 100}
</Text>
</View>
) : null;
};

Is this page still up to date? Did it work for you?

+
import * as React from "react";
import { View, TextProps, PixelRatio, AppState } from "react-native";
import { MaterialTopTabNavigationOptions } from "@react-navigation/material-top-tabs";
import { StackNavigationOptions } from "@react-navigation/stack";
import { BottomTabNavigationOptions } from "@react-navigation/bottom-tabs";
import { DrawerNavigationOptions } from "@react-navigation/drawer";
import { Text } from "./Text";

// These constants determine how much bigger the font size should get based on the user's
// accessibility settings. Even if they turn the dial all the way to 11, we will only ever
// scale the fonts by these factors. This is to prevent the font size from getting too large
// and completely breaking the layout.
const MAX_FONT_SCALE = 1.2;
const MIN_FONT_SCALE = 0.8;

// Returns fontScaling props for Text and TextInput components
// Usage:
// const fontProps = useFontScaling();
// return <Text {...fontProps}>Text Here</Text>;
export const useFontScaling = (): Partial<TextProps> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<TextProps> = React.useMemo(() => {
return {
minimumFontScale: allowFontScaling ? MIN_FONT_SCALE : 1, // This prevents the font from getting too small.
maxFontSizeMultiplier: allowFontScaling ? MAX_FONT_SCALE : 1, // This prevents the font from getting too big.
allowFontScaling, // This allows the font to be scaled or not.
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Navigator components
export const useNavigatorFontScalingScreenOptions =
(): Partial<StackNavigationOptions> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<StackNavigationOptions> = React.useMemo(() => {
return {
headerBackAllowFontScaling: allowFontScaling,
headerTitleAllowFontScaling: allowFontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Top Tab Navigator components
export const useTopTabNavigatorFontScalingScreenOptions =
(): Partial<MaterialTopTabNavigationOptions> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<MaterialTopTabNavigationOptions> =
React.useMemo(() => {
return {
tabBarAllowFontScaling: allowFontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Tab Navigator components
export const useTabNavigatorFontScalingScreenOptions =
(): Partial<BottomTabNavigationOptions> => {
// You probably want to get this value from your user's preferences
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<BottomTabNavigationOptions> =
React.useMemo(() => {
return {
tabBarAllowFontScaling: fontScaling,
headerTitleAllowFontScaling: fontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Returns fontScaling props for Tab Navigator components
export const useDrawerNavigatorFontScalingScreenOptions =
(): Partial<DrawerNavigationOptions> => {
const [allowFontScaling,] = React.useState(true);

const fontScaling: Partial<DrawerNavigationOptions> = React.useMemo(() => {
return {
drawerAllowFontScaling: allowFontScaling,
headerTitleAllowFontScaling: allowFontScaling,
};
}, [allowFontScaling]);

return fontScaling;
};

// Use this handy __DEV__ mode only component to figure out what the font size is actually doing.
export const DevFontSize = () => {
const [allowFontScaling,] = React.useState(true);
const [appStateVisible, setAppStateVisible] = React.useState(
AppState.currentState
);

React.useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
setAppStateVisible(nextAppState);
});

return () => subscription.remove();
}, []);

// This memo has to listen to appStateVisible even though it's not a direct dependency
// so that we can reload the font size when the app switches back from user settings.
const fontSize = React.useMemo(() => {
if (allowFontScaling) {
return Math.min(
Math.max(PixelRatio.getFontScale(), MIN_FONT_SCALE),
MAX_FONT_SCALE
);
} else {
return 1.0;
}
}, [allowFontScaling, appStateVisible]); // eslint-disable-line react-hooks/exhaustive-deps

return __DEV__ ? (
<View style={{
backgroundColor: '#E58F83',
padding: 10,
margin: 10,
borderRadius: 5,
borderColor: '#000,
borderWidth: 1,
}}>
<Text>
User Font Setting: {Math.trunc(PixelRatio.getFontScale() * 100) / 100}
</Text>
<Text>
Currently limiting ratio to: {Math.trunc(fontSize * 100) / 100}
</Text>
</View>
) : null;
};

Is this page still up to date? Did it work for you?

\ No newline at end of file diff --git a/docs/recipes/Authentication/index.html b/docs/recipes/Authentication/index.html new file mode 100644 index 00000000..018a6283 --- /dev/null +++ b/docs/recipes/Authentication/index.html @@ -0,0 +1,170 @@ + + + + + +Authentication with Supabase | Ignite Cookbook for React Native + + + + + + + + + + +

Authentication with Supabase

Overview

+

Many applications require an external service to authenticate the user. Setting up authentication for your application can feel daunting. Where do I start? What data do I need from my users? What service(s) should or could I use? What are the signup, signin, and other user flows that I'll need?

+

This recipe is going to use Supabase as the backend. We'll build some primitives that will allow you to customize the authentication to your needs or existing backend service as well.

+

Requirements

+

Since we're using Supabase for our backend, it is assumed that you have an account there. We're going to need two pieces of information from that account the project URL and anonymous public key. (Inside your Supabase account, visit the API credentials section.)

+

Starting Point

+

We're going to start from a freshly ignited project without any of the boilerplate screens:

+
Terminal
bunx ignite-cli@latest new AuthRecipe --workflow=cng --remove-demo --git --install-deps --packager=bun
+
info

Notice we're using Expo Continuous Native Generation (CNG). We're also using bun in this recipe, but feel free to change that to the package manager of your choice. Read more about bun

+

Once the app is ignited 🔥, we can make sure everything is working by running the app:

+
Terminal
cd AuthRecipe
bun run ios
+

Checkpoint: The iOS simulator should open up to the welcome screen of the application.

+

Build Initial Sign In Screen

+

We'll use the ignite generators to generate the Sign In screen:

+
Terminal
bunx ignite-cli@latest generate screen SignIn
+
info

bunx auto-installs and runs packages from npm. It's Bun's equivalent of npx or yarn dlx.

+

Replace the contents of that screen with the following:

+
SignInScreen.tsx
/app/screens/SignInScreen.tsx
import React, { FC, useState } from "react"
import { observer } from "mobx-react-lite"
import {
Image,
ImageStyle,
Pressable,
TextStyle,
View,
ViewStyle,
} from "react-native"
import { AppStackScreenProps } from "app/navigators"
import { Button, Screen, Text, TextField } from "app/components"
import { useSafeAreaInsetsStyle } from "app/utils/useSafeAreaInsetsStyle"
import { colors, spacing } from "app/theme"

const logo = require("../../assets/images/logo.png")

interface SignInScreenProps extends AppStackScreenProps<"SignIn"> {}

export const SignInScreen: FC<SignInScreenProps> = observer(
function SignInScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")

const onSignIn = () => {
// Sign In Flow
console.log("Sign In Flow", { email, password })
}

const onSignUp = () => {
// Sign Up Flow
console.log("Sign Up Flow")
}

const onForgotPassword = () => {
// Forgot Password Flow
console.log("Forgot Password Flow")
}

return (
<Screen
contentContainerStyle={$root}
preset="auto"
safeAreaEdges={["top"]}
>
<View style={$container}>
<View style={$topContainer}>
<Image style={$logo} source={logo} resizeMode="contain" />
</View>
<View style={[$bottomContainer, $bottomContainerInsets]}>
<View>
<TextField
containerStyle={$textField}
label="Email"
autoCapitalize="none"
defaultValue={email}
onChangeText={setEmail}
/>
<TextField
containerStyle={$textField}
label="Password"
autoCapitalize="none"
defaultValue={password}
secureTextEntry
onChangeText={setPassword}
/>
</View>
<View>
<Button onPress={onSignIn}>Sign In</Button>
<Pressable style={$forgotPassword} onPress={onForgotPassword}>
<Text preset="bold">Forgot Password?</Text>
</Pressable>
<Text style={$buttonDivider}>- or -</Text>
<Button preset="reversed" onPress={onSignUp}>
Sign Up
</Button>
</View>
<View style={$cap} />
</View>
</View>
</Screen>
)
}
)

const $root: ViewStyle = {
minHeight: "100%",
backgroundColor: colors.palette.neutral100,
}

const $container: ViewStyle = {
backgroundColor: colors.background,
}

const $topContainer: ViewStyle = {
height: 200,
justifyContent: "center",
alignItems: "center",
}

const $bottomContainer: ViewStyle = {
backgroundColor: colors.palette.neutral100,
paddingBottom: spacing.xl,
paddingHorizontal: spacing.lg,
}

const $cap: ViewStyle = {
backgroundColor: colors.palette.neutral100,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
height: spacing.xl,
position: "absolute",
top: -spacing.xl,
left: 0,
right: 0,
}

const $textField: ViewStyle = {
marginBottom: spacing.md,
}

const $forgotPassword: ViewStyle = {
marginVertical: spacing.md,
}

const $buttonDivider: TextStyle = {
textAlign: "center",
marginVertical: spacing.md,
}

const $logo: ImageStyle = {
height: 88,
width: "100%",
marginBottom: spacing.xxl,
}
+

In order for us to be able to see this new Sign In screen, let's add an isAuthenticated conditional to show the "Welcome" screen when the user is signed in and the "Sign In" screen when they are not.

+
/app/navigators/AppNavigator.tsx
const AppStack = observer(function AppStack() {
const isAuthenticated = false
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
navigationBarColor: colors.background,
}}
>
{isAuthenticated ? (
<>
{/** 🔥 Your screens go here */}
<Stack.Screen name="Welcome" component={Screens.WelcomeScreen} />
{/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */}
</>
) : (
<Stack.Screen name="SignIn" component={Screens.SignInScreen} />
)}
</Stack.Navigator>
)
})
+

This should cause the application to refresh and display our new Sign In screen. A couple of things to notice here is that we already have onPress handlers for our buttons, onSubmitEditing handlers for our inputs, and onChangeText wired up for updating the email and password state.

+
info

For brevity, we're leaving out internationalization for this recipe. For TextInput labels, we would normally add those into our translation files under the common section as those words will likely be used often.

+

Environment Config

+

We're going to take the project URL and the anonymous public key that we gathered from the Requirements section and add them to our environment.

+
/.env
EXPO_PUBLIC_SUPABASE_URL="https://<your-project-id>.supabase.co"
EXPO_PUBLIC_SUPABASE_ANON_KEY="<your-anon-public-key>"
+
tip

Why put these values inside of the environment config? When working on larger projects, it's common to have different URLs and keys for local, testing, staging, and production configurations. You may be thinking "But that's what the base config is for!" However, this assumes that every member of your team is using the same backend URL and key for development. Putting this information in the environment reduces code churn when these values change between the different environments and even team members.

+

Typically .env is not commited to version control so let's update our .gitignore to ignore this file:

+
/.gitignore
.env
+
info

Expo has great documentation on using environment variables if you'd like to know more about how that works.

+

This allows us to have different configurations for our development, staging, testing, and production environments. For our purposes, we're going to add these values to the base configuration as these props are required for every environment.

+
/app/config/config.base.ts
export interface ConfigBaseProps {
persistNavigation: "always" | "dev" | "prod" | "never"
catchErrors: "always" | "dev" | "prod" | "never"
exitRoutes: string[]
supabaseUrl: string
supabaseAnonKey: string
}

export type PersistNavigationConfig = ConfigBaseProps["persistNavigation"]

const BaseConfig: ConfigBaseProps = {
// This feature is particularly useful in development mode, but
// can be used in production as well if you prefer.
persistNavigation: "dev",

/**
* Only enable if we're catching errors in the right environment
*/
catchErrors: "always",

/**
* This is a list of all the route names that will exit the app if the back button
* is pressed while in that screen. Only affects Android.
*/
exitRoutes: ["Welcome"],
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
}

export default BaseConfig
+
note

These new environment variables will not be available until the next time you restart metro either with bun run ios or bun start.

+

Dependencies

+

For this recipe we've made some specific choices around the packages that we'll use:

+ +
Terminal
bunx expo install @supabase/supabase-js react-native-mmkv
+

Since react-native-mmkv has a host platform dependency, we'll need to also rebuild the application with:

+
Terminal
bun ios
# or
bun android
+

Session Storage

+
note

If you're already using Async Storage in your application, you can take advantage of that and skip this section.

+

We need a place to store the user's session after they login. This will allow us to log them back in after they close the application or refresh their access token after it has expired. Supabase's client is already setup for Async Storage's API. (e.g. getItem, setItem, and removeItem) We're going to use react-native-mmkv as it is not only faster, but has some additional features that we can utilize.

+
Example session storage implementation
/app/utils/storage/SessionStorage.ts
import { MMKV } from "react-native-mmkv"

const storage = new MMKV({
id: "session",
})

// TODO: Remove this workaround for encryption: https://github.com/mrousavy/react-native-mmkv/issues/665
storage.set("workaround", true)

/**
* A simple wrapper around MMKV that provides a base API
* that matches AsyncStorage for use with Supabase.
*/

/**
* Get an item from storage by key
*
* @param {string} key of the item to fetch
* @returns {Promise<string | null>} value for the key as a string or null if not found
*/
export async function getItem(key: string): Promise<string | null> {
try {
return storage.getString(key) ?? null
} catch {
console.warn(`Failed to get key "${key}" from secure storage`)
return null
}
}

/**
* Sets an item in storage by key
*
* @param {string} key of the item to store
* @param {string} value of the item to store
*/
export async function setItem(key: string, value: string): Promise<void> {
try {
storage.set(key, value)
} catch {
console.warn(`Failed to set key "${key}" in secure storage`)
}
}

/**
* Removes a single item from storage by key
*
* @param {string} key of the item to remove
*/
export async function removeItem(key: string): Promise<void> {
try {
storage.delete(key)
} catch {
console.warn(`Failed to remove key "${key}" from secure storage`)
}
}
+

Encrypting the User Session

+

If you'd like to encrypt the user's session because it contains sensitive information, you can take advantage of Expo SecureStore and MMKV's encryption. Expo SecureStore will securely store key-value pairs locally on device in the iOS keychain or Android Keystore. The reason we need both is that Expo SecureStore has a size limit of 2048 bytes. The Supabase session is already larger than 2048 bytes by default so we're going to generate a unique key with Expo Crypto to encrypt the Session Store with MMKV and store that key with Expo SecureStore.

+

First, we'll need to install those additional dependencies:

+
Terminal
bunx expo install expo-secure-store expo-crypto
+

We'll also need to add Expo SecureStorage to our plugin configuration:

+
/app.json
...
"plugins": [
"expo-localization",
"expo-secure-store",
[
"expo-build-properties",
{
"ios": {
"newArchEnabled": false,
"flipper": false
},
"android": {
"newArchEnabled": false
}
}
],
"expo-font"
],
...
+

Rebuild the application with:

+
Terminal
bun ios
# or
bun android
+

Now, we can use Expo SecureStore and Expo Crypto to securely generate and store our encrypting key for MMKV:

+
/app/utils/storage/SessionStorage.ts
import { MMKV } from "react-native-mmkv"
import * as SecureStore from "expo-secure-store"
import * as Crypto from "expo-crypto"

const fetchOrGenerateEncryptionKey = (): string => {
const encryptionKey = SecureStore.getItem("session-encryption-key")

if (encryptionKey) {
return encryptionKey
} else {
const uuid = Crypto.randomUUID()
SecureStore.setItem("session-encryption-key", uuid)
return uuid
}
}

const storage = new MMKV({
id: "session",
encryptionKey: fetchOrGenerateEncryptionKey(),
})
+
note

If you're using Async Storage and you'd also like to encrypt the user's session, refer to the Encrypting the user session section of the Supabase guide.

+

Creating and Managing the Session

+

There are three pieces that we're going to need to create and manage our session: a hook, context, and provider.

+

Initializing the Supabase Client

+

Let's start by creating the file for the hook to initialize the Supabase client with our environment config and SessionStorage we set up earlier:

+
/app/services/auth/supabase.ts
import Config from "app/config"
import { createClient } from "@supabase/supabase-js"
import * as SessionStorage from "app/utils/storage/SessionStorage"
import { AppState } from "react-native"

export const supabase = createClient(
Config.supabaseUrl,
Config.supabaseAnonKey,
{
auth: {
storage: SessionStorage,
autoRefreshToken: true,
detectSessionInUrl: false,
},
}
)

export { type Session, type AuthError } from "@supabase/supabase-js"

/**
* Tells Supabase to autorefresh the session while the application
* is in the foreground. (Docs: https://supabase.com/docs/reference/javascript/auth-startautorefresh)
*/
AppState.addEventListener("change", (nextAppState) => {
if (nextAppState === "active") {
supabase.auth.startAutoRefresh()
} else {
supabase.auth.stopAutoRefresh()
}
})
+
note

If you've opted to use Async Storage, change line 11 above to storage: AsyncStorage.

+
info

Why not use PKCE (pronounced pixy) by setting the flowType: "pkce"? It stands for "Proof Key for Code Exchange". Read Supabase's write-up about why they did it and how it works. You'd ecounter this with doing email confirmation for your sign up process as well as password resets. If you decided to turn it on, sign up will produce the console warning "WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.", but it will still work. Since we currently have email confirmation disabled, we'll save email confirmation and password reset for a future recipe.

+

Since we're allowing the Supabase client to manage session storage, it will automatically persist changes to the session. We've added an event listener to stop refreshing the session when the application is no longer in the foreground and restart it when it returns to the foreground. The Supabase client will then automatically refresh the session as necessary; one less thing that we'll need to handle manually. 😮‍💨

+

Signing Up & Signing In

+

To keep this simple, we're going to use the same form for both. We'll need to create an onPress and onSubmit handler for the respective actions that are already stubbed out in the SignInScreen we created earlier. You may have more information you'd like to capture (e.g. name, phone number, password confirmation, etc.) when a user signs up. In such a case, create a separate "Sign Up" screen that captures the additional data.

+

Creating Authentication Context & Provider

+

We're going to be using the session across components, at different depths in our component tree, and with navigation. For this access pattern we'll create a context and provider. This way if that information changes, we'll re-render the entire tree. (e.g. If the user signs out, we'll navigate back to the "Sign In" screen automatically.)

+
tip

Be careful when using contexts as anything that depends on data in the context is going to cause a re-render when the data changes. This can have performance implications if you're re-rendering the entire tree frequently. In the case of authentication, we want to re-render the entire tree when the session is updated as we may need to navigate to the sign in screen if the user's session expires.

+

Let's setup our AuthContext with our session state, add our AuthProvider, and create a useAuth hook that will return the value of our context:

+
/app/services/auth/useAuth.tsx
import React, {
createContext,
PropsWithChildren,
useCallback,
useContext,
useState,
} from "react"
import { Session, supabase } from "./supabase"
import { AuthResponse, AuthTokenResponsePassword } from "@supabase/supabase-js"

type AuthState = {
isAuthenticated: boolean
token?: Session["access_token"]
}

type SignInProps = {
email: string
password: string
}

type SignUpProps = {
email: string
password: string
}

type AuthContextType = {
signIn: (props: SignInProps) => Promise<AuthTokenResponsePassword>
signUp: (props: SignUpProps) => Promise<AuthResponse>
} & AuthState

const AuthContext = createContext<AuthContextType>({
isAuthenticated: false,
token: undefined,
signIn: () => new Promise(() => ({})),
signUp: () => new Promise(() => ({})),
})

export function useAuth() {
const value = useContext(AuthContext)

if (process.env.NODE_ENV !== "production") {
if (!value) {
throw new Error("useAuth must be used within an AuthProvider")
}
}

return value
}

export const AuthProvider = ({ children }: PropsWithChildren) => {
const [token, setToken] = useState<AuthState["token"]>(undefined)

const signIn = useCallback(
async ({ email, password }: SignInProps) => {
const result = await supabase.auth.signInWithPassword({
email,
password,
})

if (result.data?.session?.access_token) {
setToken(result.data.session.access_token)
}

return result
},
[supabase]
)

const signUp = useCallback(
async ({ email, password }: SignUpProps) => {
const result = await supabase.auth.signUp({
email,
password,
})

if (result.data?.session?.access_token) {
setToken(result.data.session.access_token)
}

return result
},
[supabase]
)

return (
<AuthContext.Provider
value={{
isAuthenticated: !!token,
token,
signIn,
signUp,
}}
>
{children}
</AuthContext.Provider>
)
}
+

Now that we have those pieces in place, we can wrap our application with the AuthProvider so that we can access the AuthContext inside of our compontents and navigation:

+
/app/app.tsx
...
import { ViewStyle } from "react-native"
import { AuthProvider } from "./services/auth/useAuth"

...

return (
<AuthProvider>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<ErrorBoundary catchErrors={Config.catchErrors}>
<GestureHandlerRootView style={$container}>
<AppNavigator
linking={linking}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</GestureHandlerRootView>
</ErrorBoundary>
</SafeAreaProvider>
</AuthProvider>
)
}
+

Next, we'll wire up the isAuthenticated to the useAuth hook inside our AppStack to show the "Sign In" screen when the user is not authenticated and the "Welcome" screen when the are:

+
/app/navigators/AppNavigator.tsx
...

import { colors } from "app/theme"
import { useAuth } from "app/services/auth/useAuth"

...

const AppStack = observer(function AppStack() {
const isAuthenticated = false
const { isAuthenticated } = useAuth()
return (
<Stack.Navigator screenOptions={{ headerShown: false, navigationBarColor: colors.background }}>
{isAuthenticated ? (
<>
{/** 🔥 Your screens go here */}
<Stack.Screen name="Welcome" component={Screens.WelcomeScreen} />
{/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */}
</>
) : (
<Stack.Screen name="SignIn" component={Screens.SignInScreen} />
)}
</Stack.Navigator>
)
})

...
+

Lastly, let's wire up the SignInScreen to use signIn and signUp from the useSession hook:

+
/app/screens/SignInScreen.tsx
...
import { colors, spacing } from "app/theme"
import { useAuth } from "app/services/auth/useAuth"

...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const { signIn, signUp } = useAuth()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")

const passwordInput = React.useRef<TextInput>(null)

const onSignIn = () => {
// Sign In Flow
console.log("Sign In Flow", { email, password })
signIn({ email, password })
}

const onSignUp = () => {
// Sign Up Flow
console.log("Sign Up Flow")
signUp({ email, password })
}

...
+
warning

Before you try to sign in for the first time, we'll want to make sure that email confirmation is turned off inside of this Supabase project. With email confirmation turned on, creating a user will only return the user and not return the session. For now, disable email confirmation in your project by clicking on Authentication > Providers > Email and toggling "Confirm Email" to off.

+

Checkpoint: With those changes you should be able to enter an email and password and press the "Sign Up" button which will create a user, return the session, and navigate you to the "Welcome" screen.

+

Signing Out

+

Oh no! We're stuck in the signed in state. No dark patterns here! Let's fix that by adding the signOut action to our useAuth hook:

+
/app/services/auth/useAuth.tsx
...
type AuthContextType = {
signIn: (props: SignInProps) => Promise<AuthTokenResponsePassword>
signUp: (props: SignUpProps) => Promise<AuthResponse>
signOut: () => void
} & AuthState

const AuthContext = createContext<AuthContextType>({
isAuthenticated: false,
token: undefined,
signIn: () => new Promise(() => ({})),
signUp: () => new Promise(() => ({})),
signOut: () => undefined,
})
...
export const AuthProvider = ({ children }: PropsWithChildren) => {
...
const signOut = useCallback(async () => {
await supabase.auth.signOut()
setToken(undefined)
}, [supabase])

return (
<AuthContext.Provider
value={{
isAuthenticated: !!token,
token,
signIn,
signUp,
signOut,
}}
>
{children}
</AuthContext.Provider>
)
}
+

Now, we'll add the "Sign Out" button and update the screen to show some data from the session:

+
/app/screens/WelcomeScreen.tsx
import { observer } from "mobx-react-lite"
import React, { FC } from "react"
import { Image, ImageStyle, TextStyle, View, ViewStyle } from "react-native"
import { Text } from "app/components"
import { Button, Text } from "app/components"
import { isRTL } from "../i18n"
import { AppStackScreenProps } from "../navigators"
import { colors, spacing } from "../theme"
import { useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle"
import { useAuth } from "app/services/auth/useAuth"

const welcomeLogo = require("../../assets/images/logo.png")
const welcomeFace = require("../../assets/images/welcome-face.png")

interface WelcomeScreenProps extends AppStackScreenProps<"Welcome"> {}

export const WelcomeScreen: FC<WelcomeScreenProps> = observer(
function WelcomeScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const { signOut } = useAuth()

return (
<View style={$container}>
<View style={$topContainer}>
<Image
style={$welcomeLogo}
source={welcomeLogo}
resizeMode="contain"
/>
<Text
testID="welcome-heading"
style={$welcomeHeading}
tx="welcomeScreen.readyForLaunch"
text="Congratulations 🎉 You're signed in!"
preset="heading"
/>
<Text tx="welcomeScreen.exciting" preset="subheading" />
<Image
style={$welcomeFace}
source={welcomeFace}
resizeMode="contain"
/>
</View>
<View style={[$bottomContainer, $bottomContainerInsets]}>
<Text tx="welcomeScreen.postscript" size="md" />
<Button onPress={signOut}>Sign Out</Button>
</View>
</View>
)
}
)
+

If you're anything like me, you may have noticed that the screen transition always sliding to the left seems off. The way we mentally feel about "Sign In" and "Sign Out" is entering and existing. The way the animations are working right now, it feels like we just keep signing in. Let's adjust that navigation transition:

+
/app/navigators/AppNavigator.tsx
...
const AppStack = observer(function AppStack() {
const { isAuthenticated } = useAuth()
return (
<Stack.Navigator screenOptions={{ headerShown: false, navigationBarColor: colors.background }}>
{isAuthenticated ? (
<>
{/** 🔥 Your screens go here */}
<Stack.Screen name="Welcome" component={Screens.WelcomeScreen} />
{/* IGNITE_GENERATOR_ANCHOR_APP_STACK_SCREENS */}
</>
) : (
<Stack.Screen
name="SignIn"
component={Screens.SignInScreen}
options={{ animationTypeForReplace: "pop" }}
/>
)}
</Stack.Navigator>
)
})
...
+

All is right with the world again. 😅

+

Checkpoint: You should now be able to sign up, sign in, and sign out. This is a good time to commit what you have.

+

Listening for Session Changes

+

As noted earlier, we're listening for changes in the AppState for when the application comes back to the foreground. However, there are other session events we should listen for such as signing out of all devices, user updates, password recovery, etc. Checkout "Listen to Auth Events" in the Supabase docs for detailed information about each event.

+

To listen for these authentication state changes, we can subscribe to those events when the application initially loads.

+
/app/services/auth/useAuth.tsx
...
import React, { createContext, PropsWithChildren, useCallback, useContext, useState } from "react"
import React, { createContext, PropsWithChildren, useCallback, useContext, useEffect, useState } from "react"
...
export const AuthProvider = ({ children }: PropsWithChildren) => {
const [token, setToken] = useState<AuthState["token"]>(undefined)

useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
switch (event) {
case "SIGNED_OUT":
setToken(undefined)
break
case "INITIAL_SESSION":
case "SIGNED_IN":
case "TOKEN_REFRESHED":
setToken(session?.access_token)
break
default:
// no-op
}
})

return () => {
subscription.unsubscribe()
}
}, [supabase])
...
+

Loading States

+

Those with a keen eye will notice that our AuthContext does not contain loading states. There's a reason for that. Loading states should be local to the UI that initiated them. Using loading states can make your application feel more responsive and set proper expectations for the user. You may have also noticed that our signIn function returns a promise. Let's add a loading state for the "Sign In" flow and (a)wait for the sign up request to complete:

+
/app/screens/SignInScreen.tsx
...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const { signIn, signUp } = useAuth()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isSigningIn, setIsSigningIn] = useState(false)

const onSignIn = () => {
signIn({ email, password })
const onSignIn = async () => {
try {
setIsSigningIn(true)
await signIn({ email, password })
} finally {
setIsSigningIn(false)
}
}
...
<Button onPress={onSignIn}>Sign In</Button>
<Button onPress={onSignIn}>
{isSigningIn ? "Signing In..." : "Sign In"}
</Button>
...

+

🙌 Easy, let's do the same thing for sign up:

+
/app/screens/SignInScreen.tsx
...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const { signIn, signUp } = useAuth()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isSigningIn, setIsSigningIn] = useState(false)
const [isSigningUp, setIsSigningUp] = useState(false)

const onSignIn = async () => {
try {
setIsSigningIn(true)
await signIn({ email, password })
} finally {
setIsSigningIn(false)
}
}

const onSignUp = () => {
signUp({ email, password })
const onSignUp = async () => {
try {
setIsSigningUp(true)
await signUp({ email, password })
} finally {
setIsSigningUp(false)
}
}
...
<Button preset="reversed" onPress={onSignUp}>Sign Up</Button>
<Button preset="reversed" onPress={onSignIn}>
{isSigningUp ? "Signing Up..." : "Sign Up"}
</Button>
...

+

Lastly, should a user be able to sign up and sign in at the same time? No. We can use a combined loading state to disable the buttons and make the text inputs read only while we are either signing in or signing up:

+
/app/screens/SignInScreen.tsx
...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const { signIn, signUp } = useAuth()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isSigningIn, setIsSigningIn] = useState(false)
const [isSigningUp, setIsSigningUp] = useState(false)
const isLoading = isSigningIn || isSigningUp
...
<View>
<TextField
containerStyle={$textField}
label="Email"
autoCapitalize="none"
defaultValue={email}
onChangeText={setEmail}
readOnly={isLoading}
/>
<TextField
containerStyle={$textField}
label="Password"
autoCapitalize="none"
defaultValue={password}
secureTextEntry
onChangeText={setPassword}
readOnly={isLoading}
/>
</View>
<View>
<Button onPress={onSignIn} disabled={isLoading}>
{isSigningIn ? "Signing In..." : "Sign In"}
</Button>
<Pressable style={$forgotPassword} onPress={onForgotPassword} disabled={isLoading}>
<Text preset="bold">Forgot Password?</Text>
</Pressable>
<Text style={$buttonDivider}>- or -</Text>
<Button preset="reversed" onPress={onSignUp} disabled={isLoading}>
{isSigningUp ? "Signing Up..." : "Sign Up"}
</Button>
</View>
</View>
+

No more double sign up or sign in requests. This bit of defensive programming is minimal additional effort that saves you and your team time down the road as your users will not encounter that issue.

+

Error Handling

+

What would you expect to happen if the user submitted an empty form for Sign In or Sign Up? What if they submit an email and no password or vice versa? What if there's a network issue? What if there's a service outage? How can we allow the user to self diagnose the issue if it's something they can correct? That's where good error handling comes in. So let's start with errors on form submission.

+

There's a reason that we return the result to the caller so we can present these errors locally to the user.

+
/app/screens/SignInScreen.tsx
...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const { signIn, signUp } = useAuth()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState<string | undefined>(undefined)
...
const onSignIn = async () => {
try {
setIsSigningIn(true)
setError(undefined)

await signIn({ email, password })
const { error } = await signIn({ email, password })
if (error) {
setError(error.message)
}
} finally {
setIsSigningIn(false)
}
}

const onSignUp = async () => {
try {
setIsSigningUp(true)
setError(undefined)

await signUp({ email, password })
const { error } = await signUp({ email, password })
if (error) {
setError(error.message)
}
} finally {
setIsSigningUp(false)
}
}
...
return (
<Screen
contentContainerStyle={$root}
preset="auto"
safeAreaEdges={["top"]}
>
<View style={$container}>
<View style={$topContainer}>
<Image style={$logo} source={logo} resizeMode="contain" />
</View>
<View style={[$bottomContainer, $bottomContainerInsets]}>
{error && <Text style={$errorText}>{error}</Text>}
<View>
...
+

Now if there is an issue with our authentication request, the user will be one step closer to understanding why. But why would we send authentication requests that we know are going to fail? We shouldn't and we'll fix that next. We're going to add some simple form validation to validate the values of our text inputs. We not only want to make sure that both text inputs have values, but that they are also valid values. (e.g. an email address)

+
/app/screens/SignInScreen.tsx
...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
const { signIn, signUp } = useAuth()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [validationErrors, setValidationErrors] = useState<Map<string, string>>(new Map())
...
const validateForm = () => {
const errors: Map<string, string> = new Map()

if (!email || email.split("@").length !== 2) {
errors.set("Email", "must be valid email")
}

if (!password) {
errors.set("Password", "cannot be blank")
}

return errors
}

const onSignIn = async () => {
try {
setIsSigningIn(true)
setError(undefined)

const errors = validateForm()
setValidationErrors(errors)
if (errors.size > 0) return

const { error } = await signIn({ email, password })
if (error) {
setError(error.message)
}
} finally {
setIsSigningIn(false)
}
}

const onSignUp = async () => {
try {
setIsSigningUp(true)
setError(undefined)

const errors = validateForm()
setValidationErrors(errors)
if (errors.size > 0) return

const { error } = await signUp({ email, password })
if (error) {
setError(error.message)
}
} finally {
setIsSigningUp(false)
}
}
...
<View>
<TextField
containerStyle={$textField}
label="Email"
autoCapitalize="none"
defaultValue={email}
onChangeText={setEmail}
readOnly={isLoading}
helper={validationErrors.get("Email")}
status={validationErrors.get("Email") ? "error" : undefined}
/>
<TextField
containerStyle={$textField}
label="Password"
autoCapitalize="none"
defaultValue={password}
secureTextEntry
onChangeText={setPassword}
readOnly={isLoading}
helper={validationErrors.get("Password")}
status={validationErrors.get("Password") ? "error" : undefined}
/>
...
+

Checkpoint: At this point, everything is working as expected and we're giving the user valuable feedback throughout the process.

+

Before we wrap this up, there is one more thing we should do for the user experience of our sign up and sign in form.

+

Form & Input Affordances

+

These are the little details that help our UI be a bit more precise, reduce mistakes, and help guide the user through the process. Tweaks like these have outsized benefits for the size of the code change.

+

One such detail is already in place; autoCapitalize="none". If you've ever tried to put in your email address only to frustratingly have the first character continually capitalized, this was the culprit.

+

Keyboard Type & Auto Complete

+

We have an email address as our first text input. Let's use the keyboard that's specific for that by setting the inputMode and setup auto complete for these fields for use with autofill:

+
/app/screens/SignInScreen.tsx
...
<TextField
autoCapitalize="none"
autoComplete="email"
autoCorrect={false}
containerStyle={$textField}
defaultValue={email}
helper={validationErrors.get("Email")}
inputMode="email"
label="Email"
onChangeText={setEmail}
readOnly={isLoading}
status={validationErrors.get("Email") ? "error" : undefined}
/>
<TextField
autoCapitalize="none"
autoComplete="current-password"
autoCorrect={false}
containerStyle={$textField}
defaultValue={password}
helper={validationErrors.get("Password")}
label="Password"
onChangeText={setPassword}
readOnly={isLoading}
secureTextEntry
status={validationErrors.get("Password") ? "error" : undefined}
/>
...
+
note

We're using current-password for auto complete for the password field since the sign in flow will be used more frequently by the user. If you split out the "Sign Up" into its own form, use new-password to give autofill a better cue for that flow. Read the autoComplete docs for all of the available options and support.

+

Keyboard Flow

+

We can also setup directives to display the "Next" and "Done" buttons on the keyboard (return key) when the user has certain fields focused. We can also direct the cursor around or trigger events when those buttons are pressed:

+
    +
  1. When the "Email" field is focused, set the return key to read "Next".
  2. +
  3. When the "Next" return key is pressed, focus the "Password" field.
  4. +
  5. When the "Password" field is focused, set the return key to read "Done"
  6. +
  7. When the "Done" return key is pressed, trigger the sign in process.
  8. +
+
/app/screens/SignInScreen.tsx
import React, { FC, useState } from "react"
import React, { FC, useRef, useState } from "react"
import { observer } from "mobx-react-lite"
import { Image, ImageStyle, Pressable, TextStyle, View, ViewStyle } from "react-native"
import { Image, ImageStyle, Pressable, TextInput, TextStyle, View, ViewStyle } from "react-native"
...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
...
const isLoading = isSigningIn || isSigningUp

const passwordInput = useRef<TextInput>(null)

const onSignIn = async () => {
...
<View>
<TextField
autoCapitalize="none"
autoComplete="email"
autoCorrect={false}
containerStyle={$textField}
defaultValue={email}
helper={validationErrors.get("Email")}
inputMode="email"
label="Email"
onChangeText={setEmail}
onSubmitEditing={() => passwordInput.current?.focus()}
readOnly={isLoading}
returnKeyType="next"
status={validationErrors.get("Email") ? "error" : undefined}
/>
<TextField
autoCapitalize="none"
autoComplete="current-password"
autoCorrect={false}
containerStyle={$textField}
defaultValue={password}
helper={validationErrors.get("Password")}
label="Password"
onChangeText={setPassword}
onSubmitEditing={onSignIn}
readOnly={isLoading}
ref={passwordInput}
returnKeyType="done"
secureTextEntry
status={validationErrors.get("Password") ? "error" : undefined}
/>
</View>
+
tip

Once the props for the component get long enough, alphabetizing them can help make that a bit more manageable.

+

Show Password

+

Thanks to Ignite's prebuilt components, adding this little bit of functionality is pretty simple. Here we'll use the demo code that Ignite projects generate with; unless you opt-out like we did.

+
/app/screens/SignInScreen.tsx
import React, { ComponentType, FC, useRef, useState } from "react"
import React, { ComponentType, FC, useMemo, useRef, useState } from "react"
import { observer } from "mobx-react-lite"
import {
Image,
ImageStyle,
Pressable,
TextInput,
TextStyle,
View,
ViewStyle,
} from "react-native"
import { AppStackParamList, AppStackScreenProps } from "app/navigators"
import { Button, Screen, Text, TextField } from "app/components"
import {
Button,
Icon,
Screen,
Text,
TextField,
TextFieldAccessoryProps,
} from "app/components"
...
export const SignInScreen: FC<SignInScreenProps> = observer(function SignInScreen() {
...
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isPasswordHidden, setIsPasswordHidden] = useState(true)
...
const PasswordRightAccessory: ComponentType<TextFieldAccessoryProps> = useMemo(
() =>
function PasswordRightAccessory(props: TextFieldAccessoryProps) {
return (
<Icon
icon={isPasswordHidden ? "view" : "hidden"}
color={colors.palette.neutral800}
containerStyle={props.style}
size={20}
onPress={() => setIsPasswordHidden(!isPasswordHidden)}
/>
)
},
[isPasswordHidden],
)

return (
<Screen
contentContainerStyle={$root}
preset="auto"
safeAreaEdges={["top"]}
>
...
<TextField
autoCapitalize="none"
autoComplete="current-password"
autoCorrect={false}
containerStyle={$textField}
defaultValue={password}
helper={validationErrors.get("Password")}
labelTx="common.password"
onChangeText={setPassword}
onSubmitEditing={onSubmit}
readOnly={isLoading}
ref={passwordInput}
returnKeyType="send"
secureTextEntry
RightAccessory={PasswordRightAccessory}
secureTextEntry={isPasswordHidden}
status={validationErrors.get("Password") ? "error" : undefined}
/>
...
+

🎉 Congratulations!

+

You now have an application that can sign users up, in, and out that handles token refresh, listens for background session changes, stores the user's session securely, handles error & loading states, and has the proper form affordances for your workflows.

Is this page still up to date? Did it work for you?

+ + \ No newline at end of file diff --git a/docs/recipes/CircleCIRNSetup/index.html b/docs/recipes/CircleCIRNSetup/index.html index 7e4b65cc..19780d4f 100644 --- a/docs/recipes/CircleCIRNSetup/index.html +++ b/docs/recipes/CircleCIRNSetup/index.html @@ -10,8 +10,8 @@ - - + +

CircleCI CD Setup - React Native

This document shows the steps necessary to set up CircleCI automatic continuous integration testing and automatic Fastlane beta builds upon successfully merging a pull request.

+

CircleCI CD Setup - React Native

This document shows the steps necessary to set up CircleCI automatic continuous integration testing and automatic Fastlane beta builds upon successfully merging a pull request.

First Things First

  1. Write Tests
  2. @@ -133,6 +133,6 @@

    Is this page still up to date? Did it work for you?

+

Is this page still up to date? Did it work for you?

\ No newline at end of file diff --git a/docs/recipes/CreatingGreateExperienceForScreenReaders/index.html b/docs/recipes/CreatingGreateExperienceForScreenReaders/index.html index 012461cc..9130a999 100644 --- a/docs/recipes/CreatingGreateExperienceForScreenReaders/index.html +++ b/docs/recipes/CreatingGreateExperienceForScreenReaders/index.html @@ -10,8 +10,8 @@ - - + +

Creating a Good Experience for Screen Readers

UI Patterns

+

Creating a Good Experience for Screen Readers

UI Patterns

Screens

Titles All screen should ideally have unique titles, to make it easier to know quickly which screen you're on source.

diff --git a/docs/recipes/DetoxIntro/index.html b/docs/recipes/DetoxIntro/index.html index 780f6d1c..3dbcd8ec 100644 --- a/docs/recipes/DetoxIntro/index.html +++ b/docs/recipes/DetoxIntro/index.html @@ -10,8 +10,8 @@ - - + +

Detox Intro

Detox is a library for end-to-end testing of React Native apps. This wiki provides information on how to use Detox effectively.

+

Detox Intro

Detox is a library for end-to-end testing of React Native apps. This wiki provides information on how to use Detox effectively.

Installation

Detox's documentation for installation.

It's included by default in Ignite.

diff --git a/docs/recipes/DistributingAuthTokenToAPI/index.html b/docs/recipes/DistributingAuthTokenToAPI/index.html index ae3c9d99..c742952f 100644 --- a/docs/recipes/DistributingAuthTokenToAPI/index.html +++ b/docs/recipes/DistributingAuthTokenToAPI/index.html @@ -10,8 +10,8 @@ - - + +

Distributing Auth Token to APISauce

+

Distributing Auth Token to APISauce

Building off of the Ignite Boilerplate, this recipe will show you how to connect your Mobx State Tree Authentication Store with an Apisauce instance to make authenticating your API requests a breeze.

Review of API Instance and Auth Store

To start off let's quickly review the boilerplate Auth Store and API Instance.

diff --git a/docs/recipes/EASUpdate/index.html b/docs/recipes/EASUpdate/index.html index 427d098c..210d046e 100644 --- a/docs/recipes/EASUpdate/index.html +++ b/docs/recipes/EASUpdate/index.html @@ -10,8 +10,8 @@ - - + +

EAS Update

+

EAS Update

This guide will teach you how to set up over-the-air (OTA) updates with Expo and EAS Update within an Ignite project.

Appetizer

You'll also need eas-cli globally installed and and an Expo account if you don't already have one.

diff --git a/docs/recipes/EnforcingImportOrder/index.html b/docs/recipes/EnforcingImportOrder/index.html index 9497b72b..fdfeecdb 100644 --- a/docs/recipes/EnforcingImportOrder/index.html +++ b/docs/recipes/EnforcingImportOrder/index.html @@ -10,8 +10,8 @@ - - + +

Enforcing JS/TS Import Order

+

Enforcing JS/TS Import Order

Overview

With Intellisense and Copilot at our fingertips, it's easy to forget that the order of imports in a file can have a big impact on the readability and maintainability of your code. Import helpers typically just put the autogenerated import at the bottom of all the other imports.

This recipe will show you how to enforce a consistent import order in your project using the @serverless-guru/prettier-plugin-import-order prettier plugin.

diff --git a/docs/recipes/EnvironmentVariables/index.html b/docs/recipes/EnvironmentVariables/index.html index 9704cef8..4bf69a5a 100644 --- a/docs/recipes/EnvironmentVariables/index.html +++ b/docs/recipes/EnvironmentVariables/index.html @@ -10,8 +10,8 @@ - - + +

Environment Variables

+

Environment Variables

Setup

Install

yarn add -D dotenv babel-plugin-inline-dotenv
diff --git a/docs/recipes/ExpoRouter/index.html b/docs/recipes/ExpoRouter/index.html index d705d8d5..b4b3b99f 100644 --- a/docs/recipes/ExpoRouter/index.html +++ b/docs/recipes/ExpoRouter/index.html @@ -10,8 +10,8 @@ - - + +

Expo Router

+

Expo Router

Overview

Expo Router brings file-based routing to React Native and web applications allowing you to easily create universal apps. Whenever a file is added to your src/app directory, a new path is automatically added to your navigation.

For the full documentation by Expo, head on over to the Introduction to Expo Router.

diff --git a/docs/recipes/GeneratorComponentTests/index.html b/docs/recipes/GeneratorComponentTests/index.html index 0c868b48..9e662efc 100644 --- a/docs/recipes/GeneratorComponentTests/index.html +++ b/docs/recipes/GeneratorComponentTests/index.html @@ -10,8 +10,8 @@ - - + +

Add component tests to npx ignite-cli generate component

+

Add component tests to npx ignite-cli generate component

Did you know that Ignite automatically generates files for you? And that you can customize those generators?

Here is how to automatically generate components and tests for them using @testing-library/react-native

Setup @testing-library/react-native

diff --git a/docs/recipes/LocalFirstDataWithPowerSync/index.html b/docs/recipes/LocalFirstDataWithPowerSync/index.html index b356801a..825dd380 100644 --- a/docs/recipes/LocalFirstDataWithPowerSync/index.html +++ b/docs/recipes/LocalFirstDataWithPowerSync/index.html @@ -10,8 +10,8 @@ - - + +

PowerSync and Supabase for Local-First Data Management

+

PowerSync and Supabase for Local-First Data Management

Introduction

This guide helps you integrate PowerSync with Supabase in an Ignite app for efficient local-first data management.

PowerSync allows your app to work smoothly offline while keeping the data in sync with your backend database.

diff --git a/docs/recipes/MaestroSetup/index.html b/docs/recipes/MaestroSetup/index.html index 7aa62fb2..30fb5e51 100644 --- a/docs/recipes/MaestroSetup/index.html +++ b/docs/recipes/MaestroSetup/index.html @@ -10,8 +10,8 @@ - - + +

Setting Up Maestro in Ignite

+

Setting Up Maestro in Ignite

Overview

End-to-end (e2e) testing is a critical part of any application but it can be difficult to set up and maintain. Maestro is a tool that promises to be easy to set up and maintain e2e tests. This recipe will walk you through setting up Maestro in your Ignite project.

Maestro Installation

diff --git a/docs/recipes/MigratingToMMKV/index.html b/docs/recipes/MigratingToMMKV/index.html index 93c98119..25193896 100644 --- a/docs/recipes/MigratingToMMKV/index.html +++ b/docs/recipes/MigratingToMMKV/index.html @@ -10,8 +10,8 @@ - - + +

Migrating to MMKV

+

Migrating to MMKV

Overview

MMKV is said to be the fastest key/value storage for React Native. It has encryption support for secure local storage and also uses synchronous storage to simplify your application code.

In this recipe, we'll convert our the Ignite demo project from using AsyncStorage to MMKV.

diff --git a/docs/recipes/PatchingBuildingAndroid/index.html b/docs/recipes/PatchingBuildingAndroid/index.html index 97d5315c..50615157 100644 --- a/docs/recipes/PatchingBuildingAndroid/index.html +++ b/docs/recipes/PatchingBuildingAndroid/index.html @@ -10,8 +10,8 @@ - - + +

Patching/Building Android .aar From Source

Why?

+

Patching/Building Android .aar From Source

Why?

Sometimes, a situation arises when you might want to update the react-native Android source code without upgrading react-native itself. For example, there's a new bug on Android 12 where the application crashes due to some bug with the animation queue. The potential fix is available on the main (unreleased) branch, but your app version is a few patches behind. Another situation is when you simply can't upgrade your react-native version yet, but need a fix from future version. In these cases, you can use this approach to "patch" your Android source files and build new .aar binary and use that for your app.

Official Guides

The official steps to build from source are provided by react-native and you can find them here.

diff --git a/docs/recipes/PrepForEASBuild/index.html b/docs/recipes/PrepForEASBuild/index.html index 379e0c80..763a7f44 100644 --- a/docs/recipes/PrepForEASBuild/index.html +++ b/docs/recipes/PrepForEASBuild/index.html @@ -10,8 +10,8 @@ - - + +

Prepping Ignite for EAS Build

+

Prepping Ignite for EAS Build

This guide will teach you how to set up an Expo development build which prepares your project for native code via Config Plugins, but keeps you in Expo's managed workflow.

Appetizer

Start with a fresh Ignite app, but choose the prebuild workflow:

diff --git a/docs/recipes/ReactNativeVisionCamera/index.html b/docs/recipes/ReactNativeVisionCamera/index.html index 1425c8e8..e8478c4e 100644 --- a/docs/recipes/ReactNativeVisionCamera/index.html +++ b/docs/recipes/ReactNativeVisionCamera/index.html @@ -10,8 +10,8 @@ - - + +

VisionCamera

+

VisionCamera

Overview

VisionCamera is a powerful, high-performance React Native Camera library. It's both feature-rich and flexible! The library provides the necessary hooks and functions to easily integrate camera functionality in your app.

In this example, we'll take a look at wiring up a barcode scanner. This tutorial is written for the Ignite v9 Prebuild workflow, however it generally still applies to DIY or even a bare react-native project.

diff --git a/docs/recipes/Redux/index.html b/docs/recipes/Redux/index.html index 0955375d..4fdca076 100644 --- a/docs/recipes/Redux/index.html +++ b/docs/recipes/Redux/index.html @@ -10,8 +10,8 @@ - - + +

Redux

+

Redux

This guide will show you how to migrate a MobX-State-Tree project (Ignite's default) to Redux, using a newly created Ignite project as our example:

npx ignite-cli new ReduxApp --yes --removeDemo

If you are migrating an existing project these steps still apply, but you may need to migrate your existing state tree and other additional functionality.

diff --git a/docs/recipes/RemoveMobxStateTree/index.html b/docs/recipes/RemoveMobxStateTree/index.html index ceb4669a..4ce3994f 100644 --- a/docs/recipes/RemoveMobxStateTree/index.html +++ b/docs/recipes/RemoveMobxStateTree/index.html @@ -10,8 +10,8 @@ - - + +

Remove Mobx-State-Tree

+

Remove Mobx-State-Tree

By default, Ignite uses MobX-State-Tree as the default state management solution. While we love MobX-State-Tree at Infinite Red, we understand the landscape is rich with great alternatives that you may want to use instead.

This guide will show you how to remove Mobx-State-Tree from an Ignite-generated project and get to a "blank slate" with no state management at all.

Steps

diff --git a/docs/recipes/RequiringHardwareFeaturesWithExpo/index.html b/docs/recipes/RequiringHardwareFeaturesWithExpo/index.html index 2ca7b275..25417364 100644 --- a/docs/recipes/RequiringHardwareFeaturesWithExpo/index.html +++ b/docs/recipes/RequiringHardwareFeaturesWithExpo/index.html @@ -10,8 +10,8 @@ - - + +

Requiring Hardware Features with Expo

+

Requiring Hardware Features with Expo

Overview

iOS and Android allow you to specify specific hardware that your app needs in order to be able to run. When users go to download your app from the respective app stores, if their device doesn't meet this hardware requirement, the store will not allow the user to download the app to that device.

For this recipe, we're going to be creating an expo config plugin to add the required properties to our app's prebuild system so that users with devices that DO NOT have a front-facing camera or a microphone won't be able to download our app.

diff --git a/docs/recipes/SampleYAMLCircleCI/index.html b/docs/recipes/SampleYAMLCircleCI/index.html index 1546563c..560fd383 100644 --- a/docs/recipes/SampleYAMLCircleCI/index.html +++ b/docs/recipes/SampleYAMLCircleCI/index.html @@ -10,8 +10,8 @@ - - + +

Sample YAML for CircleCi for Ignite

Sample YAML File

+

Sample YAML for CircleCi for Ignite

Sample YAML File

# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#

defaults: &defaults
docker:
# Choose the version of Node you want here
- image: circleci/node:10.11
working_directory: ~/repo

version: 2
jobs:
setup:
<<: *defaults
steps:
- checkout
- restore_cache:
name: Restore node modules
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: Install dependencies
command: |
yarn install
- save_cache:
name: Save node modules
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}

tests:
<<: *defaults
steps:
- checkout
- restore_cache:
name: Restore node modules
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: Install React Native CLI and Ignite CLI
command: |
sudo npm i -g ignite-cli react-native-cli
- run:
name: Run tests
command: yarn ci:test # this command will be added to/found in your package.json scripts

publish:
<<: *defaults
steps:
- checkout
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- restore_cache:
name: Restore node modules
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
# Run semantic-release after all the above is set.
- run:
name: Publish to NPM
command: yarn ci:publish # this will be added to your package.json scripts

workflows:
version: 2
test_and_release:
jobs:
- setup
- tests:
requires:
- setup
- publish:
requires:
- tests
filters:
branches:
only: master

Is this page still up to date? Did it work for you?

\ No newline at end of file diff --git a/docs/recipes/SelectFieldWithBottomSheet/index.html b/docs/recipes/SelectFieldWithBottomSheet/index.html index 85d30ae0..56319424 100644 --- a/docs/recipes/SelectFieldWithBottomSheet/index.html +++ b/docs/recipes/SelectFieldWithBottomSheet/index.html @@ -10,8 +10,8 @@ - - + +

SelectField using react-native-bottom-sheet

+

SelectField using react-native-bottom-sheet

In this guide, we'll be creating a SelectField component by extending the TextField with a scrollable options View and additional props to handle its customization.

We will be using the react-native-bottom-sheet library for the options list, the ListItem component for displaying individual options, and the TextField component for opening the options list and displaying selected options.

diff --git a/docs/recipes/SwitchBetweenExpoGoCNG/index.html b/docs/recipes/SwitchBetweenExpoGoCNG/index.html index 237861bd..6bddcce4 100644 --- a/docs/recipes/SwitchBetweenExpoGoCNG/index.html +++ b/docs/recipes/SwitchBetweenExpoGoCNG/index.html @@ -10,8 +10,8 @@ - - + +

Switch a Project Between Expo Go and Expo CNG

+

Switch a Project Between Expo Go and Expo CNG

If you created an Ignite project using the Expo Go workflow and you need to transition to Expo CNG (Continuous Native Generation) or visa versa, this guide will teach you how to reconfigure your project.

Expo Go -> Expo CNG

If you started with Expo Go but now need to add a library with native code, create your own custom native code, or modify native project configuration, you'll no longer be able to run your app inside Expo Go.

diff --git a/docs/recipes/TypeScriptBaseURL/index.html b/docs/recipes/TypeScriptBaseURL/index.html index 1e7c1cd4..74687990 100644 --- a/docs/recipes/TypeScriptBaseURL/index.html +++ b/docs/recipes/TypeScriptBaseURL/index.html @@ -10,8 +10,8 @@ - - + +

TypeScript baseUrl Configuration

+

TypeScript baseUrl Configuration

Overview

Depending on your project structure, sometimes you'll end up with longer relative imports like this:

import { Thing } from "../../../../../components/thing";
diff --git a/docs/recipes/UnrenderedItemInScrollView/index.html b/docs/recipes/UnrenderedItemInScrollView/index.html index b6c60ec6..8460b358 100644 --- a/docs/recipes/UnrenderedItemInScrollView/index.html +++ b/docs/recipes/UnrenderedItemInScrollView/index.html @@ -10,8 +10,8 @@ - - + +

Scrolling to a location that hasn't been rendered using FlatList or SectionList

Calling scrollViewRef.current.scrollToLocation() on a React Native FlatList or SectionList will fail on occasion because it can't scroll to a location that hasn't been rendered yet.

+

Scrolling to a location that hasn't been rendered using FlatList or SectionList

Calling scrollViewRef.current.scrollToLocation() on a React Native FlatList or SectionList will fail on occasion because it can't scroll to a location that hasn't been rendered yet.

The solution to this is implementing onScrollToIndexFailed with some sort of recovery functionality to keep trying the scroll. This is a Higher Order Component (HOC) for SectionList that handles this for us.

This component basically tries over and over to scroll to the requested location until it gets it right and no longer calls onScrollToIndexFailed.

import * as React from 'react';
import { SectionList, SectionListProps, SectionListScrollParams } from 'react-native';

interface SectionListHandle {
scrollToLocation: (params: SectionListScrollParams) => void;
}

/**
* This is a wrapper around react-native's SectionList that adds protection against scrolling to an
* unknown (not rendered yet) location. This is useful for cases where the user wants to scroll to a
* position very far down the list but we haven't rendered that far yet.
*
* This adds onScrollToIndexFailed property to SectionList so that if the scroll fails, we calculate the approximate
* scroll position, scroll there, and then try again to get the exact position requested.
*
* Essentially, it's a "guess the position and retry the operation" strategy until the list is scrolled to the
* correct location.
*/
export const ScrollProtectedSectionList = React.forwardRef<
SectionListHandle,
SectionListProps<any, any>
>((props, forwardedRef) => {
const internalRef = React.useRef<SectionList>(null);
const [lastScrollRequest, setLastScrollRequest] = React.useState<SectionListScrollParams>();
const timeout = React.useRef<ReturnType<typeof setTimeout>>();

const onScrollToIndexFailed = (info: {
index: number;
highestMeasuredFrameIndex: number;
averageItemLength: number;
}) => {
console.log('ScrollProtectedSectionList.onScrollToIndexFailed', info);

// Calculate the possible position of the item and scroll there using the internal scroll responder.
const offset = info.averageItemLength * info.index;
internalRef.current?.getScrollResponder()?.scrollTo({ x: 0, y: offset, animated: false });

// If we know exactly where we want to scroll to, we can just scroll now since the item is likely visible.
// Otherwise it'll call this function recursively again.
if (lastScrollRequest) {
timeout.current = setTimeout(() => {
internalRef.current?.scrollToLocation(lastScrollRequest);
}, 100);
}
};

// Clear the timeout if it still exists when the component unmounts.
React.useEffect(() => {
return () => timeout.current && clearTimeout(timeout.current);
}, []);

React.useImperativeHandle(
forwardedRef,
() => ({
scrollToLocation: (params: SectionListScrollParams) => {
internalRef.current?.scrollToLocation(params);
setLastScrollRequest(params);
},
}),
[internalRef],
);

return <SectionList {...props} ref={internalRef} onScrollToIndexFailed={onScrollToIndexFailed} />;
});

Is this page still up to date? Did it work for you?

diff --git a/docs/recipes/UpdatingDependencies/index.html b/docs/recipes/UpdatingDependencies/index.html index ed04110a..9b20d257 100644 --- a/docs/recipes/UpdatingDependencies/index.html +++ b/docs/recipes/UpdatingDependencies/index.html @@ -10,8 +10,8 @@ - - + +

Updating Dependencies with Yarn Audit, Outdated and Upgrade

If you get a bunch of warnings in the git command output about vulnerabilities, similar to this: remote: Github found 80 vulnerabilities on <branch>..., you can examine these vulnerabilities with yarn audit, get a list of outdated packages with yarn outdated, and update each dependency using yarn update

+

Updating Dependencies with Yarn Audit, Outdated and Upgrade

If you get a bunch of warnings in the git command output about vulnerabilities, similar to this: remote: Github found 80 vulnerabilities on <branch>..., you can examine these vulnerabilities with yarn audit, get a list of outdated packages with yarn outdated, and update each dependency using yarn update

Yarn Audit Checks for known security issues with the installed packages. Issue the command from the root of your project. The output is a list of known issues.

Usage:

yarn audit
diff --git a/docs/recipes/UpdatingIgnite/index.html b/docs/recipes/UpdatingIgnite/index.html index dd6f59d9..692f92db 100644 --- a/docs/recipes/UpdatingIgnite/index.html +++ b/docs/recipes/UpdatingIgnite/index.html @@ -10,8 +10,8 @@ - - + +

Updating Ignite boilerplate with ignite-diff-purge

Many React Native developers aks this question:

+

Updating Ignite boilerplate with ignite-diff-purge

Many React Native developers aks this question:

How can I update my Ignite boilerplate with the latest changes from the Ignite boilerplate?

diff --git a/docs/recipes/UsingScreenReaders/index.html b/docs/recipes/UsingScreenReaders/index.html index dbc103e4..57ad6d97 100644 --- a/docs/recipes/UsingScreenReaders/index.html +++ b/docs/recipes/UsingScreenReaders/index.html @@ -10,8 +10,8 @@ - - + +

Using Screen Readers

+

Using Screen Readers

iOS

On a simulator

Setting it up diff --git a/docs/recipes/Zustand/index.html b/docs/recipes/Zustand/index.html index 796f9a66..4a4422d2 100644 --- a/docs/recipes/Zustand/index.html +++ b/docs/recipes/Zustand/index.html @@ -10,8 +10,8 @@ - - + +

Zustand

+

Zustand

Zustand is a "bearbones" state management solution (hence the cute bear mascot). Its a relatively simple and unopinionated option to manage application state, with a hooks-based API for easy use in a React app.

This guide will show you how to migrate a MobX-State-Tree project (Ignite's default) to Zustand, using a new Ignite project as an example:

diff --git a/docs/tags/accessibility/index.html b/docs/tags/accessibility/index.html index 93523173..f5ca3e24 100644 --- a/docs/tags/accessibility/index.html +++ b/docs/tags/accessibility/index.html @@ -10,8 +10,8 @@ - - + +
\ No newline at end of file diff --git a/search/index.html b/search/index.html index 10cafb96..c094bd71 100644 --- a/search/index.html +++ b/search/index.html @@ -10,8 +10,8 @@ - - + +