Docs

Expo Quickstart

You will learn the following:

  • Install @clerk/expo
  • Set your Clerk API keys
  • Add <ClerkProvider />
  • Protect specific pages with authentication
  • Use Clerk hooks to enable users to sign in and out

Install @clerk/clerk-expo

Clerk's Expo SDK gives you access to prebuilt components, hooks, and helpers to make user authentication easier.

Run the following command to install the SDK:

terminal
npm install @clerk/clerk-expo
terminal
yarn add @clerk/clerk-expo
terminal
pnpm add @clerk/clerk-expo

Install @clerk/types (optional)

Clerk's @clerk/types package provides TypeScript type definitions.

Add the package to your project by running the following command:

terminal
npm install @clerk/types
terminal
yarn add @clerk/types
terminal
pnpm add @clerk/types
.env
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY

The <ClerkProvider> component provides session and user context to Clerk's hooks and components. It's recommended to wrap your entire app at the entry point with <ClerkProvider> to make authentication globally accessible. See the reference docs for other configuration options.

You must pass your Publishable Key as a prop to the <ClerkProvider> component.

Clerk also provides <ClerkLoaded>, which won't render its children until the Clerk API has loaded.

Add both components to your root layout as shown in the following example:

app/_layout.tsx
import { ClerkProvider, ClerkLoaded } from '@clerk/clerk-expo'
import { Slot } from 'expo-router'

export default function RootLayout() {
  const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!

  if (!publishableKey) {
    throw new Error('Add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to your .env file')
  }

  return (
    <ClerkProvider publishableKey={publishableKey}>
      <ClerkLoaded>
        <Slot />
      </ClerkLoaded>
    </ClerkProvider>
  )
}

Configure the token cache

Clerk stores the active user's session token in memory by default. In Expo apps, the recommended way to store sensitive data, such as tokens, is by using expo-secure-store which encrypts the data before storing it.

To use expo-secure-store as your token cache:

  1. Run the following command to install the library:

    terminal
    npm install expo-secure-store
    terminal
    yarn add expo-secure-store
    terminal
    pnpm add expo-secure-store
  2. In your root directory, create a cache.ts file and add the following code:

    cache.ts
    import * as SecureStore from 'expo-secure-store'
    import { Platform } from 'react-native'
    import { TokenCache } from '@clerk/clerk-expo/dist/cache'
    
    const createTokenCache = (): TokenCache => {
      return {
        getToken: async (key: string) => {
          try {
            const item = await SecureStore.getItemAsync(key)
            if (item) {
              console.log(`${key} was used 🔐 \n`)
            } else {
              console.log('No values stored under key: ' + key)
            }
            return item
          } catch (error) {
            console.error('secure store get item error: ', error)
            await SecureStore.deleteItemAsync(key)
            return null
          }
        },
        saveToken: (key: string, token: string) => {
          return SecureStore.setItemAsync(key, token)
        },
      }
    }
    
    // SecureStore is not supported on the web
    export const tokenCache = Platform.OS !== 'web' ? createTokenCache() : undefined
  3. Update your root layout to use the token cache:

    app/_layout.tsx
    import { tokenCache } from '@/cache'
    import { ClerkProvider, ClerkLoaded } from '@clerk/clerk-expo'
    import { Slot } from 'expo-router'
    
    export default function RootLayout() {
      const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
    
      if (!publishableKey) {
        throw new Error('Add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env')
      }
    
      return (
        <ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
          <ClerkLoaded>
            <Slot />
          </ClerkLoaded>
        </ClerkProvider>
      )
    }

Tip

When you sign a user out with signOut(), Clerk will remove the user's session JWT from the token cache.

Add sign-up and sign-in pages

Clerk currently only supports control components for Expo native. UI components are only available for Expo web. Instead, you must build custom flows using Clerk's API. The following sections demonstrate how to build custom email/password sign-up and sign-in flows. If you want to use different authentication methods, such as passwordless or OAuth, see the dedicated custom flow guides.

Layout page

First, protect your sign-up and sign-in pages.

  1. Create an (auth) route group. This will group your sign-up and sign-in pages.
  2. In the (auth) group, create a _layout.tsx file.
  3. Paste the following code. The useAuth() hook is used to access the user's authentication state. If the user is already signed in, they will be redirected to the home page.
app/(auth)/_layout.tsx
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@clerk/clerk-expo'

export default function AuthRoutesLayout() {
  const { isSignedIn } = useAuth()

  if (isSignedIn) {
    return <Redirect href={'/'} />
  }

  return <Stack />
}

Sign-up page

  1. In the (auth) group, create a sign-up.tsx file.
  2. Paste the following code. The useSignUp() hook is used to create a sign-up flow. The user can sign up using their email and password and will receive an email verification code to confirm their email.
app/(auth)/sign-up.tsx
import * as React from 'react'
import { Text, TextInput, Button, View } from 'react-native'
import { useSignUp } from '@clerk/clerk-expo'
import { useRouter } from 'expo-router'

export default function SignUpScreen() {
  const { isLoaded, signUp, setActive } = useSignUp()
  const router = useRouter()

  const [emailAddress, setEmailAddress] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [pendingVerification, setPendingVerification] = React.useState(false)
  const [code, setCode] = React.useState('')

  // Handle submission of sign-up form
  const onSignUpPress = async () => {
    if (!isLoaded) return

    // Start sign-up process using email and password provided
    try {
      await signUp.create({
        emailAddress,
        password,
      })

      // Send user an email with verification code
      await signUp.prepareEmailAddressVerification({ strategy: 'email_code' })

      // Set 'pendingVerification' to true to display second form
      // and capture OTP code
      setPendingVerification(true)
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  // Handle submission of verification form
  const onVerifyPress = async () => {
    if (!isLoaded) return

    try {
      // Use the code the user provided to attempt verification
      const signUpAttempt = await signUp.attemptEmailAddressVerification({
        code,
      })

      // If verification was completed, set the session to active
      // and redirect the user
      if (signUpAttempt.status === 'complete') {
        await setActive({ session: signUpAttempt.createdSessionId })
        router.replace('/')
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signUpAttempt, null, 2))
      }
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  if (pendingVerification) {
    return (
      <>
        <Text>Verify your email</Text>
        <TextInput
          value={code}
          placeholder="Enter your verification code"
          onChangeText={(code) => setCode(code)}
        />
        <Button title="Verify" onPress={onVerifyPress} />
      </>
    )
  }

  return (
    <View>
      <>
        <Text>Sign up</Text>
        <TextInput
          autoCapitalize="none"
          value={emailAddress}
          placeholder="Enter email"
          onChangeText={(email) => setEmailAddress(email)}
        />
        <TextInput
          value={password}
          placeholder="Enter password"
          secureTextEntry={true}
          onChangeText={(password) => setPassword(password)}
        />
        <Button title="Continue" onPress={onSignUpPress} />
      </>
    </View>
  )
}

Sign-in page

  1. In the (auth) group, create a sign-in.tsx file.
  2. Paste the following code. The useSignIn() hook is used to create a sign-in flow. The user can sign in using email address and password, or navigate to the sign-up page.
app/(auth)/sign-in.tsx
import { useSignIn } from '@clerk/clerk-expo'
import { Link, useRouter } from 'expo-router'
import { Text, TextInput, Button, View } from 'react-native'
import React from 'react'

export default function Page() {
  const { signIn, setActive, isLoaded } = useSignIn()
  const router = useRouter()

  const [emailAddress, setEmailAddress] = React.useState('')
  const [password, setPassword] = React.useState('')

  // Handle the submission of the sign-in form
  const onSignInPress = React.useCallback(async () => {
    if (!isLoaded) return

    // Start the sign-in process using the email and password provided
    try {
      const signInAttempt = await signIn.create({
        identifier: emailAddress,
        password,
      })

      // If sign-in process is complete, set the created session as active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({ session: signInAttempt.createdSessionId })
        router.replace('/')
      } else {
        // If the status isn't complete, check why. User might need to
        // complete further steps.
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }, [isLoaded, emailAddress, password])

  return (
    <View>
      <TextInput
        autoCapitalize="none"
        value={emailAddress}
        placeholder="Enter email"
        onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
      />
      <TextInput
        value={password}
        placeholder="Enter password"
        secureTextEntry={true}
        onChangeText={(password) => setPassword(password)}
      />
      <Button title="Sign in" onPress={onSignInPress} />
      <View>
        <Text>Don't have an account?</Text>
        <Link href="/sign-up">
          <Text>Sign up</Text>
        </Link>
      </View>
    </View>
  )
}

For more information about building these custom flows, including guided comments in the code examples, see the Build a custom email/password authentication flow guide.

Protect your pages

You can control which content signed-in and signed-out users can see with Clerk's prebuilt control components. For this quickstart, you'll use:

  • <SignedIn>: Children of this component can only be seen while signed in.
  • <SignedOut>: Children of this component can only be seen while signed out.

To get started, create a (home) route group with the following layout file:

app/(home)/_layout.tsx
import { Stack } from 'expo-router/stack'

export default function Layout() {
  return <Stack />
}

Then, in the same folder, create an index.tsx file and add the following code. It displays the user's email if they're signed in, or sign-in and sign-up links if they're not:

app/(home)/index.tsx
import { SignedIn, SignedOut, useUser } from '@clerk/clerk-expo'
import { Link } from 'expo-router'
import { Text, View } from 'react-native'

export default function Page() {
  const { user } = useUser()

  return (
    <View>
      <SignedIn>
        <Text>Hello {user?.emailAddresses[0].emailAddress}</Text>
      </SignedIn>
      <SignedOut>
        <Link href="/(auth)/sign-in">
          <Text>Sign in</Text>
        </Link>
        <Link href="/(auth)/sign-up">
          <Text>Sign up</Text>
        </Link>
      </SignedOut>
    </View>
  )
}

Create your first user

Run your project with the following command:

terminal
npm start
terminal
yarn start
terminal
pnpm start

Now visit your app's homepage at http://localhost:8081. Sign up to create your first user.

Enable OTA updates

Though not required, it is recommended to implement over-the-air (OTA) updates in your Expo app. This enables you to easily roll out Clerk's feature updates and security patches as they're released without having to resubmit your app to mobile marketplaces.

See the expo-updates library to learn how to get started.

Next steps

OAuth with Expo

Learn more how to build a custom OAuth flow with Expo.

MFA with Expo

Learn more how to build a custom multi-factor authentication flow with Expo.

Read session and user data

Learn how to read session and user data with Expo.

Sign-up and sign-in flow

Learn how to build a custom sign-up and sign-in authentication flow.

Feedback

What did you think of this content?

Last updated on