0Pricing
React Native Academy · 课时

构建简单的登录表单

创建电子邮件和密码表单,验证输入长度,为无效输入显示错误消息,并在提交时调用模拟登录函数。

构建简单的登录表单 是 CoddyKit 上的免费 React Native Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 React Native Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 React Native Academy 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Login Form Requirements

A login form is one of the first screens in almost every mobile app. A good implementation must: render email and password fields, validate input (non-empty, valid email format, minimum password length), show field-level error messages below each input, display a loading indicator during the authentication request, show a server error if credentials are wrong, and successfully navigate to the main app on success. This lesson applies TextInput, controlled components, validation logic, and basic state management in one realistic feature.

// State needed for the login form:
// - email: string
// - password: string
// - emailError: string
// - passwordError: string
// - isLoading: boolean
// - serverError: string

// Derived:
// - isSubmitDisabled: !email || !password || isLoading

Form State and Structure

Set up the form's state variables with useState. Using separate states for each field makes updating and validating them independently straightforward. Group the error messages alongside the input values so validation can set and clear them independently. The isLoading flag disables the submit button and shows a spinner while the authentication network request is in progress, preventing double submissions. Clear the serverError whenever the user changes any input value to give them a fresh start.

import { useState } from 'react';

function useLoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [emailError, setEmailError] = useState('');
  const [passwordError, setPasswordError] = useState('');
  const [serverError, setServerError] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  function handleEmailChange(text) {
    setEmail(text);
    if (serverError) setServerError('');
    if (emailError) setEmailError('');
  }

  function handlePasswordChange(text) {
    setPassword(text);
    if (serverError) setServerError('');
    if (passwordError) setPasswordError('');
  }

  return { email, password, emailError, passwordError, serverError, isLoading, handleEmailChange, handlePasswordChange, setEmailError, setPasswordError, setServerError, setIsLoading };
}

Validation Logic

Write a validate() function that checks each field and sets error messages. For email, check that it is non-empty and matches a basic email regex pattern. For password, check that it is at least 8 characters long. The function returns a boolean — true if all fields pass. Only run validation when the user attempts to submit (not on every keystroke, which is annoying). Return early from the submit handler if validation fails, before making any network request. This pattern keeps validation logic separate from the UI and the API call.

function validateForm({ email, password, setEmailError, setPasswordError }) {
  let valid = true;

  // Email validation
  if (!email.trim()) {
    setEmailError('Email is required');
    valid = false;
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    setEmailError('Enter a valid email address');
    valid = false;
  } else {
    setEmailError('');
  }

  // Password validation
  if (!password) {
    setPasswordError('Password is required');
    valid = false;
  } else if (password.length < 8) {
    setPasswordError('Password must be at least 8 characters');
    valid = false;
  } else {
    setPasswordError('');
  }

  return valid;
}

The Submit Handler

The submit handler orchestrates validation, the API call, and error handling. It first validates the form and returns early if invalid. Then it sets isLoading to true, makes a mock (or real) authentication request, and handles the response. On success, navigate to the home screen. On failure (wrong credentials), display the server error message. Always reset isLoading in a finally block to ensure the spinner disappears even if the request throws an unexpected error.

async function handleSubmit({
  email, password,
  setEmailError, setPasswordError,
  setServerError, setIsLoading,
  onSuccess
}) {
  if (!validateForm({ email, password, setEmailError, setPasswordError })) return;

  setIsLoading(true);
  setServerError('');

  try {
    // Replace with your real auth API call:
    const result = await mockLogin(email, password);
    if (result.success) {
      onSuccess(result.user);
    } else {
      setServerError('Invalid email or password. Please try again.');
    }
  } catch (error) {
    setServerError('Network error. Please check your connection.');
  } finally {
    setIsLoading(false);
  }
}

async function mockLogin(email, password) {
  await new Promise(r => setTimeout(r, 1200)); // simulate network
  if (email === 'test@example.com' && password === 'password123') {
    return { success: true, user: { name: 'Test User' } };
  }
  return { success: false };
}

Styled Input Field with Error

Build a reusable FormField component that wraps a styled TextInput with a label above and an error message below. The border color changes to red when there's an error. The FormField accepts all relevant TextInput props plus label and error strings. Using a wrapper component avoids repeating the label/error/border pattern for every input in your form. The error text is conditionally rendered — no empty space appears when there is no error.

import { View, Text, TextInput, StyleSheet } from 'react-native';

export default function FormField({ label, error, style, ...inputProps }) {
  return (
    <View style={styles.container}>
      <Text style={styles.label}>{label}</Text>
      <TextInput
        style={[
          styles.input,
          error ? styles.inputError : styles.inputNormal,
          style,
        ]}
        placeholderTextColor='#aaa'
        {...inputProps}
      />
      {error ? <Text style={styles.error}>{error}</Text> : null}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { gap: 4 },
  label: { fontSize: 14, fontWeight: '600', color: '#555' },
  input: { borderWidth: 1.5, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 13, fontSize: 16, backgroundColor: '#fff' },
  inputNormal: { borderColor: '#ddd' },
  inputError: { borderColor: '#e74c3c', backgroundColor: '#fff8f8' },
  error: { fontSize: 12, color: '#e74c3c' },
});

Assembling the Login Screen UI

Put the form together: a logo or app name at the top, the email and password FormField components, the server error banner, the submit button (showing a spinner when loading), and a 'Sign up' link at the bottom. Wrap everything in KeyboardAvoidingView so the keyboard doesn't cover the input fields. Use ScrollView inside so tapping outside dismisses the keyboard. The design follows a clean, vertically centered card layout that looks natural on any phone screen size.

import { KeyboardAvoidingView, ScrollView, View, Text, TouchableOpacity, ActivityIndicator, Platform, StyleSheet } from 'react-native';
import FormField from './FormField';

export default function LoginScreen({ onSuccess, onNavigateToSignUp }) {
  const { email, password, emailError, passwordError, serverError, isLoading, handleEmailChange, handlePasswordChange, setEmailError, setPasswordError, setServerError, setIsLoading } = useLoginForm();

  function submit() {
    handleSubmit({ email, password, setEmailError, setPasswordError, setServerError, setIsLoading, onSuccess });
  }

  return (
    <KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
      <ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps='handled'>
        <Text style={styles.title}>Welcome Back</Text>
        <Text style={styles.subtitle}>Sign in to your account</Text>

        {serverError ? <Text style={styles.serverError}>{serverError}</Text> : null}

        <FormField label='Email' value={email} onChangeText={handleEmailChange} error={emailError} keyboardType='email-address' autoCapitalize='none' autoCorrect={false} returnKeyType='next' />
        <FormField label='Password' value={password} onChangeText={handlePasswordChange} error={passwordError} secureTextEntry autoCapitalize='none' returnKeyType='done' onSubmitEditing={submit} />

        <TouchableOpacity style={[styles.btn, isLoading && styles.btnLoading]} onPress={submit} disabled={isLoading}>
          {isLoading ? <ActivityIndicator color='#fff' /> : <Text style={styles.btnText}>Sign In</Text>}
        </TouchableOpacity>
      </ScrollView>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  content: { flexGrow: 1, justifyContent: 'center', padding: 24, gap: 16 },
  title: { fontSize: 30, fontWeight: '800', color: '#111' },
  subtitle: { fontSize: 16, color: '#666', marginBottom: 8 },
  serverError: { backgroundColor: '#fff0f0', borderWidth: 1, borderColor: '#fcc', borderRadius: 10, padding: 12, fontSize: 14, color: '#c0392b' },
  btn: { backgroundColor: '#4f86f7', padding: 16, borderRadius: 12, alignItems: 'center', marginTop: 8 },
  btnLoading: { backgroundColor: '#aac4f7' },
  btnText: { color: '#fff', fontWeight: '700', fontSize: 16 },
});

Forgot Password Link

Add a 'Forgot Password?' link below the password field. This is a TouchableOpacity with a Text that calls a navigation callback prop. Position it with alignSelf: 'flex-end' so it appears right-aligned below the password field — a common, familiar placement. The link opens a password-reset flow (entering email → receive a reset link), which is typically a separate screen or modal. Keep the login screen focused on the sign-in flow and delegate password reset to its own component.

import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';

export default function ForgotPasswordLink({ onPress }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
      style={styles.link}
      accessibilityRole='link'
      accessibilityLabel='Forgot password? Tap to reset'
    >
      <Text style={styles.linkText}>Forgot Password?</Text>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  link: { alignSelf: 'flex-end' },
  linkText: { color: '#4f86f7', fontSize: 14, fontWeight: '500' },
});

Sign Up Navigation Link

Most login screens include a 'Don't have an account? Sign up' text link at the bottom. Build this as a row containing plain and linked text. Use nested Text components to mix plain text with the tappable link inline. This is preferable to two separate buttons because it looks cleaner and matches the typographic convention users expect. Wrap the tappable portion in a Text with onPress and link styling, nested inside the outer Text.

import { Text, View, StyleSheet } from 'react-native';

export default function SignUpLink({ onNavigate }) {
  return (
    <View style={styles.row}>
      <Text style={styles.text}>
        Don't have an account?{' '}
        <Text
          style={styles.link}
          onPress={onNavigate}
          accessibilityRole='link'
        >
          Sign up
        </Text>
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  row: { alignItems: 'center', marginTop: 8 },
  text: { fontSize: 15, color: '#666' },
  link: { color: '#4f86f7', fontWeight: '600' },
});

Loading and Success States

The login flow has several distinct states that need visual treatment. During the API call (isLoading: true), replace the button text with an ActivityIndicator and disable the button to prevent duplicate requests. On success, navigate away immediately — don't show a success message that the user sees briefly before navigation. On failure, show the server error message prominently and re-enable the button so the user can try again. Shake animation on the error card is a polished touch that draws the eye to the error without requiring the user to scroll.

import { ActivityIndicator, TouchableOpacity, Text, StyleSheet } from 'react-native';

export default function SubmitButton({ isLoading, onPress, disabled }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={disabled || isLoading}
      style={[
        styles.btn,
        (disabled && !isLoading) && styles.btnDisabled,
        isLoading && styles.btnLoading,
      ]}
      activeOpacity={0.8}
    >
      {isLoading
        ? <ActivityIndicator size='small' color='#fff' />
        : <Text style={styles.label}>Sign In</Text>
      }
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  btn: { backgroundColor: '#4f86f7', padding: 16, borderRadius: 12, alignItems: 'center', minHeight: 52 },
  btnDisabled: { backgroundColor: '#c4d8ff' },
  btnLoading: { backgroundColor: '#3a72e0' },
  label: { color: '#fff', fontWeight: '700', fontSize: 16 },
});

Biometric Authentication Option

Enhance the login screen with a biometric authentication option (Face ID / Touch ID / fingerprint) using expo-local-authentication. Check if the device supports biometrics and if the user has enrolled credentials, then show a biometric login button. On success, retrieve the stored credentials from Keychain/SecureStore and perform the login. Biometric login dramatically improves the experience for returning users who don't want to type their password every session.

import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
// Install: npx expo install expo-local-authentication expo-secure-store
import { TouchableOpacity, Text } from 'react-native';
import { useEffect, useState } from 'react';

function useBiometrics() {
  const [available, setAvailable] = useState(false);
  useEffect(() => {
    LocalAuthentication.hasHardwareAsync().then(has => {
      if (has) LocalAuthentication.isEnrolledAsync().then(setAvailable);
    });
  }, []);
  return available;
}

export default function BiometricButton({ onSuccess }) {
  const biometricAvailable = useBiometrics();
  if (!biometricAvailable) return null;

  async function authenticate() {
    const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'Sign in to your account' });
    if (result.success) {
      const credentials = await SecureStore.getItemAsync('savedCredentials');
      if (credentials) onSuccess(JSON.parse(credentials));
    }
  }

  return (
    <TouchableOpacity onPress={authenticate} style={{ alignItems: 'center', padding: 16 }}>
      <Text style={{ fontSize: 36 }}>👤</Text>
      <Text style={{ color: '#4f86f7', fontSize: 14, marginTop: 4 }}>Use Face ID</Text>
    </TouchableOpacity>
  );
}

Social Login Buttons

Many apps offer Sign in with Apple and Sign in with Google as alternatives to email/password login. Apple sign-in is required by Apple's App Store guidelines when you offer any third-party social login. Use expo-apple-authentication for Apple and @react-native-google-signin/google-signin for Google. These buttons must follow platform design guidelines — Apple provides a strict button appearance spec. Place social login buttons above the email/password form with an 'OR' divider to give users a clear choice between methods.

import * as AppleAuthentication from 'expo-apple-authentication';
// npx expo install expo-apple-authentication
import { Platform, View, Text, StyleSheet } from 'react-native';

export default function SocialLogins({ onAppleSuccess, onGoogleSuccess }) {
  return (
    <View style={styles.container}>
      {Platform.OS === 'ios' && (
        <AppleAuthentication.AppleAuthenticationButton
          buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
          buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
          cornerRadius={10}
          style={styles.appleBtn}
          onPress={async () => {
            try {
              const credential = await AppleAuthentication.signInAsync({
                requestedScopes: [
                  AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
                  AppleAuthentication.AppleAuthenticationScope.EMAIL,
                ],
              });
              onAppleSuccess(credential);
            } catch (e) {
              if (e.code !== 'ERR_CANCELED') console.error(e);
            }
          }}
        />
      )}
      <View style={styles.divider}>
        <View style={styles.line} />
        <Text style={styles.orText}>OR</Text>
        <View style={styles.line} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { gap: 16 },
  appleBtn: { width: '100%', height: 48 },
  divider: { flexDirection: 'row', alignItems: 'center', gap: 12 },
  line: { flex: 1, height: 1, backgroundColor: '#e0e0e0' },
  orText: { color: '#999', fontWeight: '500' },
});

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: validate-on-submit avoids premature error messages during typing, isLoading state disables the submit button and shows a spinner during authentication requests, and FormField wrapper components keep label, input, and error display colocated for reuse across forms. Next up we explore multi-screen navigation with React Navigation.

常见问题解答

「构建简单的登录表单」课时是免费的吗?

是的 — 「构建简单的登录表单」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 React Native Academy 课程的其余内容,请升级到 CoddyKit PRO。 React Native Academy 课程共包含 4 节课。

「构建简单的登录表单」这节课中我会学到什么?

创建电子邮件和密码表单,验证输入长度,为无效输入显示错误消息,并在提交时调用模拟登录函数。 你通过在浏览器中直接运行的动手代码来练习 React Native Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 React Native Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 React Native Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「构建简单的登录表单」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 React Native Academy 课中编写并运行代码吗?

能。每节 React Native Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. TextInput 基础与键盘类型
  2. 使用 TouchableOpacity 与 Pressable 处理点击
  3. 切换按钮、开关与复选框
  4. 构建简单的登录表单
← 返回 React Native Academy