JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Начальное состояние формы, обновление после действия (Nex...

Начальное состояние формы, обновление после действия (Next.js 15, useFormState)

Модуль 4: Node.js, Next.js и Angular
10 уровень , 6 лекция
Открыта

1. Проблемы обновления формы

Если вы уже попробовали реализовать простую форму с Server Actions, то могли заметить: после отправки формы страница не обновляется полностью, но состояние самой формы (например, поля или сообщения об ошибках) не сбрасываются автоматически. А если нужно отобразить результат действия — например, показать пользователю сообщение об успехе или ошибке, или обновить часть интерфейса — тут возникает вопрос: как это сделать красиво и современно?

Вот тут на сцену выходит решение — хук useFormState. Он помогает управлять состоянием формы: хранить результат последнего действия, показывать ошибки, сбрасывать поля, обновлять список задач и так далее. По сути, это современная альтернатива useState, но заточенная под работу с Server Actions.

Вспоминаем, как работает useFormState

useFormState — это React-хук из Next.js, который связывает форму с серверным действием (Server Action). Он позволяет:

  • Хранить и отображать результат последнего вызова Server Action (например, ошибки валидации, новые данные и т.д.)
  • Передавать начальное состояние (например, "пустая форма" или "список задач")
  • Автоматически обновлять интерфейс после отправки формы, не перезагружая страницу

Главная идея: когда пользователь отправляет форму, Server Action возвращает новое состояние, и useFormState его подхватывает. Это похоже на Redux, только проще и без всей этой "магии" с редьюсерами.

2. Базовый пример: форма добавления задачи

Давайте посмотрим на простой пример: у нас есть ToDo-лист, и мы хотим добавлять задачи через форму. После добавления задача должна появиться в списке, а поле ввода — очиститься.

Шаг 1: Создаём серверное действие


// app/actions.js (или app/page.js, если всё в одном файле)
'use server';

export async function addTodo(prevState, formData) {
  const title = formData.get('title');
  if (!title || title.trim().length < 3) {
    // Возвращаем новое состояние с ошибкой
    return {
      ...prevState,
      error: 'Название должно быть не короче 3 символов',
    };
  }
  // Добавляем задачу (в реальном приложении — запись в БД)
  const newTodos = [...prevState.todos, { title }];
  return {
    todos: newTodos,
    error: null,
  };
}

Шаг 2: Используем useFormState в компоненте


import { addTodo } from './actions';
import { useFormState } from 'react-dom';

const initialState = {
  todos: [],
  error: null,
};

export default function TodoApp() {
  // Подключаем useFormState: [state, formAction]
  const [state, formAction] = useFormState(addTodo, initialState);

  return (
    <div>
      <h1>Мои задачи</h1>
      <form action={formAction}>
        <input name="title" placeholder="Новая задача" />
        <button type="submit">Добавить</button>
      </form>
      {/* Выводим ошибку, если есть */}
      {state.error && <div style={{ color: 'red' }}>{state.error}</div>}
      <ul>
        {state.todos.map((todo, idx) => (
          <li key={idx}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

Что здесь происходит?

  • useFormState принимает два аргумента: функцию-действие (Server Action) и начальное состояние.
  • Возвращает [state, formAction]: актуальное состояние и функцию для атрибута action в форме.
  • После отправки формы Server Action возвращает новое состояние, и оно становится доступно в state.

3. Как устроено начальное состояние формы

Начальное состояние — это просто объект (или любой другой тип), который вы передаёте вторым аргументом в useFormState. Обычно это структура, которую вы хотите видеть до первого действия пользователя: например, пустой список задач, пустые поля, отсутствие ошибок.

const initialState = {
  todos: [],
  error: null,
};

Если вы хотите, чтобы при заходе на страницу в списке уже были какие-то задачи, вы можете получить их на сервере и передать в компонент через пропсы, а потом использовать как initialState.

Как обновляется состояние после действия

Вся магия в том, что после каждой отправки формы Server Action вызывается с текущим состоянием и данными формы. Он возвращает новое состояние, которое тут же попадает в компонент — без перезагрузки страницы и без ручного вызова setState.

Важный момент: Server Action всегда получает на вход сначала предыдущее состояние (prevState), а затем formData. Это позволяет, например, наращивать список задач или возвращать новые ошибки.

Пример с ошибкой

Допустим, пользователь ввёл слишком короткое название задачи:

  1. Пользователь нажимает "Добавить".
  2. Server Action видит, что длина меньше 3, и возвращает состояние с ошибкой.
  3. Компонент мгновенно показывает ошибку, не добавляя задачу.

4. Сброс полей формы после действия

Типичный вопрос: как очистить поля формы после успешного добавления?

  • По умолчанию, если поле <input> не управляется через value/controlled-компонент, браузер сам сбрасывает его после submit.
  • Если поле управляется через value и useState (controlled), то нужно вручную сбрасывать состояние поля (например, через useRef или useEffect, когда state.todos изменился).
  • Для большинства случаев с useFormState достаточно использовать неуправляемый <input> — тогда он очистится сам.

Пример с управляемым полем и сбросом


import { useRef, useEffect } from 'react';

export default function TodoApp() {
  const [state, formAction] = useFormState(addTodo, initialState);
  const inputRef = useRef(null);

  useEffect(() => {
    if (!state.error) {
      // Если задача добавлена успешно — очищаем поле
      inputRef.current.value = '';
    }
  }, [state.todos, state.error]);

  return (
    <form action={formAction}>
      <input name="title" ref={inputRef} />
      <button>Добавить</button>
    </form>
  );
}

5. Передача начального состояния из Server Component

Часто начальное состояние нужно получить на сервере (например, загрузить список задач из базы данных). В этом случае:

  1. Получаем данные в Server Component (например, через async function).
  2. Передаём их как initialState в useFormState.

Пример


// app/page.js (Server Component)
import TodoApp from './TodoApp';

export default async function Page() {
  const todos = await getTodosFromDB();
  const initialState = { todos, error: null };
  return <TodoApp initialState={initialState} />;
}

// app/TodoApp.js (Client Component)
'use client';
import { useFormState } from 'react-dom';
import { addTodo } from './actions';

export default function TodoApp({ initialState }) {
  const [state, formAction] = useFormState(addTodo, initialState);
  // ...
}

6. Практический пример: форма регистрации с ошибкой

Рассмотрим форму регистрации, где после отправки мы хотим показать ошибку или поздравление с успешной регистрацией. Начальное состояние — пустая ошибка.


// actions.js
'use server';
export async function registerUser(prevState, formData) {
  const email = formData.get('email');
  if (!email || !email.includes('@')) {
    return { error: 'Некорректный email' };
  }
  // Тут могла бы быть проверка в базе данных...
  return { error: null, message: 'Успешная регистрация!' };
}

// RegistrationForm.js
'use client';
import { useFormState } from 'react-dom';
import { registerUser } from './actions';

const initialState = { error: null, message: null };

export default function RegistrationForm() {
  const [state, formAction] = useFormState(registerUser, initialState);

  return (
    <form action={formAction}>
      <input name="email" placeholder="Email" />
      <button>Зарегистрироваться</button>
      {state.error && <div style={{ color: 'red' }}>{state.error}</div>}
      {state.message && <div style={{ color: 'green' }}>{state.message}</div>}
    </form>
  );
}

7. Полезные нюансы

Как работает обновление после действия: “магия” под капотом

Каждый раз, когда вы отправляете форму, Next.js:

  1. Отправляет данные формы и текущее состояние на сервер (в ваш Server Action).
  2. Server Action возвращает новое состояние (например, обновлённый список задач, ошибку и т.д.).
  3. useFormState автоматически обновляет компонент с этим состоянием.
  4. Если вы используете неуправляемые поля (<input name="..."> без value), браузер сам очищает их после submit.

Важно: useFormState не перезагружает страницу, не вызывает setState вручную и не требует сложной логики. Всё работает “из коробки”.

Когда стоит использовать useFormState

  • Когда форма должна показывать результат отправки (например, ошибки, новые данные, сообщения).
  • Когда нужно обновить часть интерфейса после действия (например, добавить элемент в список).
  • Когда хочется избавиться от ручного состояния useState и всей этой “клиентской” возни.

Если форма совсем простая (например, просто отправить данные и забыть), можно обойтись и без useFormState.

8. Типичные ошибки при работе с useFormState

Ошибка №1: неправильная структура состояния.
Если Server Action возвращает не тот объект, который ожидает компонент, вы получите ошибки. Например, если initialState — это { todos: [], error: null }, а Server Action возвращает просто строку, компонент "расстроится".

Ошибка №2: забыли передать начальное состояние.
Если не передать initialState или передать неправильное, useFormState не сможет корректно отрисовать компонент.

Ошибка №3: попытка использовать useFormState на сервере.
Этот хук работает только в Client Component. Если попробовать использовать его в Server Component, получите ошибку.

Ошибка №4: управляемые поля не сбрасываются.
Если вы используете controlled input с value и useState, поле не очистится само после submit. Нужно сбрасывать вручную (например, через useEffect или useRef).

Ошибка №5: мутирование состояния.
Не мутируйте prevState напрямую в Server Action! Всегда возвращайте новый объект (return { ...prevState, ... }).

Ошибка №6: забыли про асинхронность.
Server Action должен быть async-функцией, чтобы корректно работать с асинхронными операциями (например, запросами к БД).

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ