React Native Academy · Урок

Создание простой формы входа

Создайте форму для электронной почты и пароля, проверяйте длину введённых данных, показывайте сообщение об ошибке при некорректном вводе и вызывайте имитацию функции входа при отправке формы.

Урок 4 из 413 шагов

«Создание простой формы входа» — бесплатный урок React Native Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения 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.

Можно начать бесплатно

Изучай JavaScript с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
30
Уроки
120

Часто задаваемые вопросы

Урок «Создание простой формы входа» бесплатный?

Да — полный текст урока «Создание простой формы входа» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс React Native Academy, подпишись на CoddyKit PRO. Курс React Native Academy содержит 4 уроков всего.

Чему я научусь в уроке «Создание простой формы входа»?

Создайте форму для электронной почты и пароля, проверяйте длину введённых данных, показывайте сообщение об ошибке при некорректном вводе и вызывайте имитацию функции входа при отправке формы. Ты практикуешь React Native Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать React Native Academy?

Предыдущий опыт не требуется. React Native Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Создание простой формы входа»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке React Native Academy?

Да. Каждый урок React Native Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Основы TextInput и типы клавиатуры
  2. Обработчики нажатий с TouchableOpacity и Pressable
  3. Переключатели и флажки
  4. Создание простой формы входа
← Назад к React Native Academy