Login + register screens

Forms + validation + errors.

9 minmedium

Build the login + register screens β€” forms, validation, error states. The Grit mobile scaffold ships these pre-built; this lesson explains every piece so you can customize them.

The screens

app/(auth)/_layout.tsx wrapper β€” centered card, no tabs
app/(auth)/login.tsx email + password
app/(auth)/register.tsx email + name + password

The form pattern

app/(auth)/login.tsx
import { useState } from 'react'
import { View, TextInput, Text, Pressable } from 'react-native'
import { router } from 'expo-router'
import { useAuth } from '@/hooks/use-auth'
export default function LoginScreen() {
const { login, isLoading } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
async function onSubmit() {
setError(null)
try {
await login(email, password)
router.replace('/(tabs)')
} catch (err) {
if (err.code === 'INVALID_CREDENTIALS') setError('Wrong email or password.')
else setError(err.message)
}
}
return (
<View className="flex-1 p-6">
<Text className="text-2xl font-semibold mb-6">Sign in</Text>
<TextInput
autoCapitalize="none"
keyboardType="email-address"
textContentType="emailAddress"
autoComplete="email"
placeholder="Email"
value={email}
onChangeText={setEmail}
className="border rounded p-3 mb-3"
/>
<TextInput
secureTextEntry
autoComplete="password"
textContentType="password"
placeholder="Password"
value={password}
onChangeText={setPassword}
className="border rounded p-3 mb-3"
/>
{error && <Text className="text-red-500 mb-3">{error}</Text>}
<Pressable
onPress={onSubmit}
disabled={isLoading}
className="bg-blue-600 rounded p-3 items-center"
>
<Text className="text-white">{isLoading ? 'Signing in…' : 'Sign in'}</Text>
</Pressable>
</View>
)
}

The bits that matter for mobile UX

  • autoCapitalize="none" on the email field β€” otherwise iOS capitalises the first letter
  • keyboardType="email-address" β€” shows the email keyboard with @ readily accessible
  • autoComplete + textContentType β€” triggers the OS password autofill from 1Password / iCloud Keychain / Bitwarden
  • secureTextEntry on the password field β€” masks input, disables autocorrect, disables screenshots in some OS configs
Hardware keyboard hint: on a physical phone, the keyboard takes half the screen. Wrap the form in a KeyboardAvoidingView + ScrollView so the focused input is always visible above the keyboard. The scaffold does this automatically.

Validation with shared Zod schemas

From the previous chapter, you have LoginSchema in packages/shared/src/schemas/. Use it client-side too:

import { LoginSchema } from '@grit/shared/schemas/auth'
async function onSubmit() {
const result = LoginSchema.safeParse({ email, password })
if (!result.success) {
setError(result.error.flatten().fieldErrors.email?.[0] ?? 'Invalid input')
return
}
await login(email, password)
}

Now the same validation runs on mobile, web, and on the API. Bad input never reaches the server.

Useful UX touches

  • Show a loading spinner on the button instead of the text. Use ActivityIndicator from React Native.
  • Disable the button while submitting to prevent double-taps.
  • Auto-focus the email field on mount with autoFocus={true}.
  • Show inline field errors below each input, not just a single error string at the top.

Quick check

A user enters `alex@example.com` and a wrong password. The API returns 401 INVALID_CREDENTIALS. Your screen shows the system error 'Network request returned 401'. What's the right fix?

Try it

On your scaffolded mobile app, sign up a new user and sign in:

  1. Run the API (cd apps/api && go run ./cmd/server)
  2. Run the mobile app (cd apps/mobile && pnpm dev)
  3. Open the app on simulator or Expo Go
  4. Navigate to /register, create an account
  5. You should land on the home screen with the user list

Paste a screenshot in notes.md.

What's next

Tokens stored where? SecureStore + Keychain β€” the OS-level secret store. Next lesson.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled β€” suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub