← Назад к вопросам

Создать форму регистрации с валидацией на React

1.7 Middle🔥 191 комментариев
#React#State Management

Условие

Создайте React-компонент формы регистрации с валидацией полей в реальном времени.

Требования

  1. Поля формы:

    • Email (валидация формата)
    • Пароль (минимум 8 символов, буквы и цифры)
    • Подтверждение пароля (должно совпадать)
    • Имя пользователя (от 3 до 20 символов)
  2. Валидация:

    • Показывать ошибки в реальном времени при вводе
    • Блокировать кнопку отправки при наличии ошибок
    • Показывать зелёную галочку для валидных полей
  3. Использовать функциональные компоненты и хуки

Бонус

Добавьте индикатор силы пароля (слабый/средний/сильный).

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Задача на валидирующую форму регистрации — классическая, показывает понимание управления состоянием и валидации в React. Создадим полное решение.

Решение 1: Базовая форма с валидацией

import React, { useState, useMemo } from "react";

interface FormErrors {
  email?: string;
  password?: string;
  confirmPassword?: string;
  username?: string;
}

interface FormData {
  email: string;
  password: string;
  confirmPassword: string;
  username: string;
}

function RegistrationForm() {
  const [formData, setFormData] = useState<FormData>({
    email: "",
    password: "",
    confirmPassword: "",
    username: "",
  });

  // Валидация
  const errors = useMemo<FormErrors>(() => {
    const newErrors: FormErrors = {};

    // Валидация email
    if (formData.email) {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(formData.email)) {
        newErrors.email = "Некорректный формат email";
      }
    } else {
      newErrors.email = "Email обязателен";
    }

    // Валидация пароля
    if (formData.password) {
      if (formData.password.length < 8) {
        newErrors.password = "Пароль должен быть минимум 8 символов";
      } else if (!/[a-zA-Z]/.test(formData.password)) {
        newErrors.password = "Пароль должен содержать буквы";
      } else if (!/\d/.test(formData.password)) {
        newErrors.password = "Пароль должен содержать цифры";
      }
    } else {
      newErrors.password = "Пароль обязателен";
    }

    // Валидация подтверждения пароля
    if (formData.confirmPassword) {
      if (formData.confirmPassword !== formData.password) {
        newErrors.confirmPassword = "Пароли не совпадают";
      }
    } else {
      newErrors.confirmPassword = "Подтверждение пароля обязательно";
    }

    // Валидация имени пользователя
    if (formData.username) {
      if (formData.username.length < 3) {
        newErrors.username = "Имя должно быть минимум 3 символа";
      } else if (formData.username.length > 20) {
        newErrors.username = "Имя должно быть максимум 20 символов";
      } else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
        newErrors.username = "Имя может содержать только буквы, цифры и подчёркивание";
      }
    } else {
      newErrors.username = "Имя обязательно";
    }

    return newErrors;
  }, [formData]);

  const isFormValid = Object.keys(errors).length === 0 &&
    Object.values(formData).every(val => val.length > 0);

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (isFormValid) {
      console.log("Форма отправлена:", formData);
      // Отправка на сервер
    }
  };

  return (
    <form onSubmit={handleSubmit} className="w-full max-w-md mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">Регистрация</h1>

      {/* Email */}
      <div className="mb-4">
        <label htmlFor="email" className="block font-medium mb-1">
          Email
        </label>
        <div className="relative">
          <input
            id="email"
            name="email"
            type="email"
            value={formData.email}
            onChange={handleChange}
            className={`w-full px-3 py-2 border rounded ${
              errors.email ? "border-red-500" : "border-gray-300"
            }`}
          />
          {!errors.email && formData.email && (
            <span className="absolute right-3 top-2 text-green-500"></span>
          )}
        </div>
        {errors.email && (
          <p className="text-red-500 text-sm mt-1">{errors.email}</p>
        )}
      </div>

      {/* Username */}
      <div className="mb-4">
        <label htmlFor="username" className="block font-medium mb-1">
          Имя пользователя
        </label>
        <div className="relative">
          <input
            id="username"
            name="username"
            type="text"
            value={formData.username}
            onChange={handleChange}
            className={`w-full px-3 py-2 border rounded ${
              errors.username ? "border-red-500" : "border-gray-300"
            }`}
          />
          {!errors.username && formData.username && (
            <span className="absolute right-3 top-2 text-green-500"></span>
          )}
        </div>
        {errors.username && (
          <p className="text-red-500 text-sm mt-1">{errors.username}</p>
        )}
      </div>

      {/* Password */}
      <div className="mb-4">
        <label htmlFor="password" className="block font-medium mb-1">
          Пароль
        </label>
        <div className="relative">
          <input
            id="password"
            name="password"
            type="password"
            value={formData.password}
            onChange={handleChange}
            className={`w-full px-3 py-2 border rounded ${
              errors.password ? "border-red-500" : "border-gray-300"
            }`}
          />
          {!errors.password && formData.password && (
            <span className="absolute right-3 top-2 text-green-500"></span>
          )}
        </div>
        {errors.password && (
          <p className="text-red-500 text-sm mt-1">{errors.password}</p>
        )}
      </div>

      {/* Confirm Password */}
      <div className="mb-6">
        <label htmlFor="confirmPassword" className="block font-medium mb-1">
          Подтверждение пароля
        </label>
        <div className="relative">
          <input
            id="confirmPassword"
            name="confirmPassword"
            type="password"
            value={formData.confirmPassword}
            onChange={handleChange}
            className={`w-full px-3 py-2 border rounded ${
              errors.confirmPassword ? "border-red-500" : "border-gray-300"
            }`}
          />
          {!errors.confirmPassword && formData.confirmPassword && (
            <span className="absolute right-3 top-2 text-green-500"></span>
          )}
        </div>
        {errors.confirmPassword && (
          <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={!isFormValid}
        className={`w-full py-2 rounded font-medium ${
          isFormValid
            ? "bg-blue-500 text-white hover:bg-blue-600"
            : "bg-gray-300 text-gray-500 cursor-not-allowed"
        }`}
      >
        Зарегистрироваться
      </button>
    </form>
  );
}

export default RegistrationForm;

Решение 2: С индикатором силы пароля (бонус)

interface PasswordStrength {
  level: "слабый" | "средний" | "сильный";
  score: number;
}

function getPasswordStrength(password: string): PasswordStrength {
  let score = 0;

  // Длина
  if (password.length >= 8) score += 1;
  if (password.length >= 12) score += 1;
  if (password.length >= 16) score += 1;

  // Буквы
  if (/[a-z]/.test(password)) score += 1;
  if (/[A-Z]/.test(password)) score += 1;

  // Цифры и спецсимволы
  if (/\d/.test(password)) score += 1;
  if (/[^a-zA-Z\d]/.test(password)) score += 2;

  // Определяем уровень
  if (score <= 2) return { level: "слабый", score };
  if (score <= 4) return { level: "средний", score };
  return { level: "сильный", score };
}

function PasswordInput({
  value,
  onChange,
  error,
}: {
  value: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  error?: string;
}) {
  const strength = getPasswordStrength(value);
  const strengthColors = {
    слабый: "bg-red-500",
    средний: "bg-yellow-500",
    сильный: "bg-green-500",
  };

  return (
    <div className="mb-4">
      <label htmlFor="password" className="block font-medium mb-1">
        Пароль
      </label>
      <input
        id="password"
        name="password"
        type="password"
        value={value}
        onChange={onChange}
        className={`w-full px-3 py-2 border rounded ${
          error ? "border-red-500" : "border-gray-300"
        }`}
      />

      {value && (
        <div className="mt-2">
          <div className="flex gap-1 mb-1">
            {[1, 2, 3].map((i) => (
              <div
                key={i}
                className={`flex-1 h-2 rounded ${
                  strength.score >= i * 2
                    ? strengthColors[strength.level]
                    : "bg-gray-200"
                }`}
              />
            ))}
          </div>
          <p className="text-sm">
            Сила пароля:{" "}
            <span className={`font-medium ${
              strength.level === "слабый" ? "text-red-500" :
              strength.level === "средний" ? "text-yellow-500" :
              "text-green-500"
            }`}>
              {strength.level}
            </span>
          </p>
        </div>
      )}

      {error && <p className="text-red-500 text-sm mt-1">{error}</p>}
    </div>
  );
}

Решение 3: С кастомным хуком для валидации

interface UseFormValidationOptions {
  validators: Record<string, (value: string) => string | undefined>;
}

function useFormValidation<T extends Record<string, string>>(
  initialValues: T,
  options: UseFormValidationOptions
) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState<Record<string, string | undefined>>({});

  const validateField = (name: string, value: string) => {
    const validator = options.validators[name];
    return validator ? validator(value) : undefined;
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));

    // Валидация в реальном времени
    const error = validateField(name, value);
    setErrors(prev => ({ ...prev, [name]: error }));
  };

  const isFormValid = Object.values(errors).every(err => !err) &&
    Object.values(values).every(val => val.length > 0);

  return { values, errors, handleChange, isFormValid };
}

// Использование
const validators = {
  email: (value: string) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return !regex.test(value) ? "Некорректный email" : undefined;
  },
  password: (value: string) => {
    if (value.length < 8) return "Минимум 8 символов";
    if (!/[a-zA-Z]/.test(value)) return "Нужны буквы";
    if (!/\d/.test(value)) return "Нужны цифры";
    return undefined;
  },
  username: (value: string) => {
    if (value.length < 3) return "Минимум 3 символа";
    if (value.length > 20) return "Максимум 20 символов";
    if (!/^[a-zA-Z0-9_]+$/.test(value)) return "Некорректные символы";
    return undefined;
  },
};

function FormWithHook() {
  const { values, errors, handleChange, isFormValid } = useFormValidation(
    { email: "", username: "", password: "", confirmPassword: "" },
    { validators }
  );

  return (
    <form>
      {/* Использование хука */}
    </form>
  );
}

Лучшие практики

  1. useMemo — оптимизируем пересчёты валидации
  2. Реальное время — ошибки показываются при вводе
  3. Визуальная обратная связь — зелёные галочки и красные границы
  4. Кастомные хуки — переиспользуемая логика валидации
  5. Регулярные выражения — правильная валидация формата

Рекомендации для собеседования

  1. Начните с базовой формы с валидацией
  2. Объясните логику проверки ошибок
  3. Добавьте индикатор силы пароля (бонус)
  4. Покажите кастомный хук (очень хорошо)
  5. Обсудите UX — когда показывать ошибки, как блокировать отправку

Лучший выбор для production: Вариант с кастомным хуком + индикатор силы пароля — профессионально и переиспользуемо.