Forms, Validation, Secure Storage and Authentication

React Native with Expo: 10 Real Projects · Advanced Apps and Release · 4 min read · Updated

Login flow

  1. The form checks the fields and shows messages for mistakes.
  2. Valid input goes to the server over HTTPS, which checks the password.
  3. The server returns a token, which the app keeps in Keychain or Keystore.
  4. Protected screens read the token. Without it they redirect to login.

Almost every real app needs a login or sign-up form, and the data it handles is sensitive. This lesson shows how to build forms that give clear feedback, where to store a login token, and how to protect screens. It uses short examples that you can add to the apps you built. It does not need a project of its own.

Forms and validation

A form is state plus rules. Keep each field in state, validate on submit (and on blur for a nicer feel) and show the message next to the field. Return errors from a plain function so you can test it.

export type Errors = { email?: string; password?: string };

export function validateLogin(email: string, password: string): Errors {
  const errors: Errors = {};
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) errors.email = "Enter a valid email";
  if (password.length < 8) errors.password = "Use at least 8 characters";
  return errors;
}
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState<Errors>({});

const submit = () => {
  const found = validateLogin(email, password);
  setErrors(found);
  if (Object.keys(found).length === 0) signIn(email.trim(), password);
};

<TextInput value={email} onChangeText={setEmail} keyboardType="email-address"
  autoCapitalize="none" autoComplete="email" placeholder="Email" />
{errors.email && <Text style={{ color: "#c92a2a" }}>{errors.email}</Text>}
<TextInput value={password} onChangeText={setPassword} secureTextEntry
  autoComplete="password" placeholder="Password" />
{errors.password && <Text style={{ color: "#c92a2a" }}>{errors.password}</Text>}

Use the right keyboard type, turn off auto-capitalisation for emails, and secureTextEntry for passwords. Wrap long forms in KeyboardAvoidingView and a ScrollView with keyboardShouldPersistTaps="handled" so the keyboard does not hide the fields. For big forms, libraries such as React Hook Form with a schema validator like Zod save work.

Client-side checks are for user experience only. The server must validate everything again, because anyone can send requests without your app.

Where to store a token

After login, your server returns a token that proves who the user is. AsyncStorage is not encrypted, so it is not the place for it. Use expo-secure-store, which keeps values in the iOS Keychain and the Android Keystore.

import * as SecureStore from "expo-secure-store";

const TOKEN = "auth.token";
export const saveToken = (t: string) => SecureStore.setItemAsync(TOKEN, t);
export const readToken = () => SecureStore.getItemAsync(TOKEN);
export const clearToken = () => SecureStore.deleteItemAsync(TOKEN);

Install it with npx expo install expo-secure-store. Keep tokens short-lived and refresh them as your backend allows. Send them in a header, never in the URL: fetch(url, { headers: { Authorization: \Bearer \${token}\ } }).

Protecting screens

A simple pattern with Expo Router keeps signed-in screens in one group and redirects when there is no session. Keep the session in a small Context or Zustand store that loads the token at start.

// app/(app)/_layout.tsx  -- only signed-in users reach these screens
import { Redirect, Stack } from "expo-router";
import { useSession } from "../../src/session";

export default function AppLayout() {
  const { token, loading } = useSession();
  if (loading) return null;               // or a splash screen
  if (!token) return <Redirect href="/login" />;
  return <Stack />;
}

Files under app/(app)/ are protected, and app/login.tsx stays open. The parentheses make a group that does not appear in the URL.

Security habits

  • Never put secrets, private keys or API keys with write power in the app.
  • Use HTTPS only.
  • Never log passwords or tokens.
  • Do not trust data from the client on the server.
  • For social login, use the provider's official method (such as expo-auth-session) rather than building your own.

Your task

  1. Add the validation function and form to a new login screen in one of your projects.
  2. Write three tests for validateLogin (bad email, short password, valid input).
  3. Store a fake token with SecureStore after pressing Sign in and read it on app start.
  4. Make a protected group and check that you are redirected when no token exists.

How to check it worked

  • Invalid input shows messages without a request being sent.
  • The token is saved in secure storage, not AsyncStorage.
  • Protected screens redirect to the login screen.

Frequently asked questions

Why not keep the token in AsyncStorage?

AsyncStorage is plain, unencrypted storage. SecureStore uses the operating system's secure storage, which is much harder for other apps or tools to read.

Is client-side validation enough?

No. It improves the experience, but a server must validate every request because attackers can skip your app.

Should I build my own login server?

Most teams use a trusted service or a well-tested library for authentication rather than writing password handling themselves.