0Pricing
React Native Academy · Lección

Creación de un formulario de inicio de sesión sencillo

Cree un formulario de correo electrónico y contraseña que valide la longitud de las entradas, muestre un mensaje de error para las entradas no válidas y llame a una función de inicio de sesión simulada al enviarlo.

Creación de un formulario de inicio de sesión sencillo es una lección gratuita de React Native Academy en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de React Native Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de React Native Academy incluye 4 lecciones en total.

Partes de esta lección aún no han sido traducidas y se muestran en inglés.

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.

Preguntas frecuentes

¿La lección «Creación de un formulario de inicio de sesión sencillo» es gratis?

Sí — el texto completo de «Creación de un formulario de inicio de sesión sencillo» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de React Native Academy, actualiza a CoddyKit PRO. El curso de React Native Academy incluye 4 lecciones en total.

¿Qué aprenderé en «Creación de un formulario de inicio de sesión sencillo»?

Cree un formulario de correo electrónico y contraseña que valide la longitud de las entradas, muestre un mensaje de error para las entradas no válidas y llame a una función de inicio de sesión simula… Practicas React Native Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar React Native Academy?

No se requiere experiencia previa. React Native Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Creación de un formulario de inicio de sesión sencillo»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de React Native Academy?

Sí. Cada lección de React Native Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Conceptos básicos de TextInput y tipos de teclado
  2. Manejadores de pulsación con TouchableOpacity y Pressable
  3. Toggles, switches y casillas de verificación
  4. Creación de un formulario de inicio de sesión sencillo
← Volver a React Native Academy