Photo by Esteban López on Unsplash
Photo by Esteban López on Unsplash

Привет, бравый покоритель фронтенда! Если ты когда-либо ковырялся в React и думал: «Эх, как же устроить красивую и понятную обработку состояния?», то вот новсть: есть такой хук - useActionState, и он может стать твоим лучшим другом.

Но постой! Разве раньше не было чего-то похожего под названием useFormState? И почему одни разработчики импортируют это из react-dom, а другие из react? Если такие вопросы посещали твою светлую голову, то добро пожаловать: сейчас мы разберёмся, почему useFormState ушёл на пенсию и чем так хорош useActionState.

Коротко о былом

В экспериментальных (Canary) версиях React существовал хук useFormState, который жил в пакете react-dom. Он помогал управлять состоянием формы и выполнял действия еще до того, как JavaScript успевал проснуться - всё звучит круто, да? Но в какой-то момент команда React решила, что функциональность этого хука гораздо шире, чем просто «форма». Так на его место пришёл useActionState, и обитает он уже не в react-dom, а прямо в react.

Иногда можно встретить статьи или примеры, где написано:

import { useFormState } from 'react-dom';

Но на дворе уже 19-я версия React (или ты можешь быть на другом экспериментальном канале), и useFormState больше не актуален. Теперь всё внимание на:

import { useActionState } from 'react';

Что это и как это работает

useActionState - это хук для тех, кто любит поддерживать порядок. Он позволяет управлять состоянием через «действия», то есть какие-то команды вроде «сделай это сейчас». При этом он чувствует себя отлично не только с формами, но и с любыми другими элементами, которые требуют обновления состояния при совершении какого-то действия.

Сигнатура проста:

const [currentState, actionFunction, isPending] = useActionState(fn, initialState, permalink?);
  • currentState - актуальное состояние (любого типа, который ты ему назначишь);

  • actionFunction - функция (или значение), которую можно прикреплять, например, к formAction или вызывать вручную;

  • isPending - булевый флажок, который подскажет, находится ли действие в процессе выполнения (полезно для спиннеров, загрузок и прочих индикаторов).

А теперь подробнее о каждом параметре, который ты передаёшь в сам useActionState:

  • fn - Главная функция, которую вызываем при действии (отправка формы, клик и т.д.).

    • Принимает предыдущее состояние (или начальное, если вызывается впервые)

    • Также получает все аргументы, которые обычно передаются при действии (например, FormData)

    • Возвращает новое состояние, которое затем попадает в currentState

  • initialState - Начальное значение для состояния. Может быть чем угодно (число, строка, объект).

    • Используется только один раз, до первого вызова fn

    • После первого экшена итоговое состояние будет всегда возвращаться из fn

  • permalink (необязательный) - Уникальный URL, куда перенаправляется форма, если JavaScript ещё не загрузился.

    • Полезно на динамических страницах (ленты товаров, блоги и т.д.)

    • На целевом URL должна рендериться та же форма (с тем же fn и permalink), чтобы React правильно передал состояние

    • После гидратации этот параметр не используется, так как весь дальнейший рендеринг идёт на клиенте

Если тебе этот хук напоминает useState на стероидах — значит, ты на верном пути!

Почему не просто useState?

useState- замечательный хук, и он с лёгкостью покрывает 90% базовых задач. Но бывают ситуации, когда в компоненте возникает 3–4 (или больше) состояний, связанных именно с результатом действия (например, сообщение об успехе/ошибке, флаг загрузки, дополнительная информация с сервера). Пример

const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [toastMessage, setToastMessage] = useState('');
// И это только начало...

Каждый раз заводить новый useState для вспомогательного статуса/ошибки — утомительно, и код становится громоздким. Вот тут useActionState бьёт кулаком по столу и говорит: «Позволь, я покажу класс!». Он позволяет хранить все «результаты» в одном объекте (например, { success, error, message, }) плюс имеет встроенный флаг isPending. Проще говоря, если перед тобой стоит задача «отправить данные и получить ответ» — лучше взять useActionState, чем лепить кучу маленьких useState .

Стоит учитывать, что когда говорят, что «useActionState может заменить множество useState», обычно имеют в виду именно вспомогательные состояния (статусы, ошибки, тексты уведомлений). Если же у тебя большая форма с десятью полями (имя, почта, пароль и т. д.), и ты хочешь управлять ими «всё в одном месте», то useActionState не подходит для всей логики: он не создан для многоэтапной работы над каждым полем. В таких случаях либо оставляют поля неконтролируемыми, либо используют useState/useReducer для управления значениями самих инпутов. А useActionState дополнительно помогает показать, как «форма отправилась и вернулась с сервера» — то есть результат и статус запроса.

Отличия от useFormState

Вкратце: useFormState жил в react-dom и подразумевал, что ты работаешь именно с формами. useActionState переехал в react и теперь его сфера применения не ограничена тегом <form>. Он работает с любыми действиями, будь то клик по кнопке, отправка формы, или вызов асинхронной функции, — и это чертовски удобно!

  • Подходит для применения не только внутри форм, но и в других интерфейсных элементах, таких как кнопки или обработчики событий.

  • Дружит с Server Components, позволяя обрабатывать состояние на сервере ещё до загрузки JS.

  • Название с «action» в имени как бы намекает, что дело не только в формах.

Базовый пример

Что может быть лучше счётчика? Только пекарня! BunBakery - место, где мы добавим на прилавок столько булочек, сколько захотите!

import { useActionState } from 'react';

const bakeBun = async (previousCount, formData) => {
  return previousCount + 1;
};

export default function BunBakery() {
  // Начальное количество булочек на прилавке - 0
  const [bunsCount, bakeBunAction, isBaking] = useActionState(bakeBun, 0);

  return (
    <div>
      <p>Булочек на прилавке: {bunsCount}</p>
      <form>
        <button formAction={bakeBunAction} disabled={isBaking}>
          {isBaking ? 'Пеку пеку...' : 'Испечь булочку'}
        </button>
      </form>
    </div>
  );
}

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

  • Когда вы нажимаете кнопку, вызывается bakeBunAction, который внутри себя запускает функцию bakeBun().

  • Пока булочка «печётся» (да, даже если это занимает доли секунды), флажок isBaking будет равен true.

  • Как только процесс завершится, количество булочек на прилавке (bunsCount) увеличится на 1.

Выглядит крайне лаконично. И это только вершина айсберга!

Пример «Загрузка файла»

Давай посмотрим, как useActionState справляется с чем-то более серьёзным, чем простой счётчик. Например, создадим компонент для загрузки файлов -UploadForm.

import { useActionState } from 'react';

// Функция обработки загрузки файла
async function handleFileUpload(prevState, formData) {
  try {
    await new Promise(resolve => setTimeout(resolve, 2000));
    return { success: true, message: 'Файл успешно загружен!' };
  } catch (error) {
    return { success: false, message: 'Ошибка при загрузке файла.' };
  }
}

function UploadForm() {
  const [uploadStatus, uploadFileAction, isUploading] = useActionState(handleFileUpload, null);

  return (
    <form>
      <input type="file" name="file" />
      <button formAction={uploadFileAction} disabled={isUploading}>
        {isUploading ? 'Загружаем...' : 'Загрузить файл'}
      </button>

      {uploadStatus && (
        <p className={uploadStatus.success ? 'success' : 'error'}>
          {uploadStatus.message}
        </p>
      )}
    </form>
  );
}

export default UploadForm;

Здесь есть несколько моментов:

  • uploadStatus - наше текущее состояние, которое может содержать что угодно: сообщение об успехе/ошибке, дополнительные поля и т. д.

  • uploadFileAction - «экшен», который прикрепляется к кнопке через атрибут formAction. Он запускает процесс загрузки файла.

  • isUploading - булевый флаг, показывающий, что файл загружается, и отключающий кнопку, чтобы избежать повторных кликов во время процесса.

Серверные функции и Server Components

Одна из важных «фишек» useActionState — это работа с Server Components и так называемыми «серверными действиями». Если ты используешь Next.js 13+ или другой фреймворк, поддерживающий React Server Components, то можешь вызывать серверную функцию напрямую:

import { useActionState } from 'react';
import { getDataFromServer } from './actions.server.js'; // "use server"

export default function ServerDataFetcher() {
  // fetchData — «экшен», который запускает запрос на сервер
  const [fetchedData, fetchData, isPending] = useActionState(async (prevData) => {
    const result = await getDataFromServer();
    return result;
  }, null);

  return (
    <div>
      <button onClick={() => fetchData()} disabled={isPending}>
        {isPending ? 'Загружаем...' : 'Получить данные с сервера'}
      </button>
      {fetchedData && (
        <p>Ответ сервера: {fetchedData}</p>
      )}
    </div>
  );
}

// actions.server.js
"use server";

export async function getDataFromServer() {
  // Имитируем запрос к серверу
  return 'Привет от сервера!';
}

Даже если JavaScript на клиенте отключен или не успел загрузиться, Server Components могут обработать твой запрос и передать готовый ответ. А когда клиент «проснётся» (гидратация завершится), React сопоставит обновлённое состояние с интерфейсом, так что пользователю всё равно всё будет работать «из коробки».

Best practices и мини-шпаргалка

  • Инициализируй состояние так, чтобы оно отражало реальный объект, с которым предстоит работать.

  • Обрабатывай ошибки возвращая из своей функции объект с error или success, чтобы в интерфейсе легко было показать соответствующее сообщение.

  • Пользуйся isPending. Не бойся показывать индикатор загрузки или дизейблить кнопку. Пользователи любят понимать, что что-то происходит.

  • Не ограничивай себя формами. Если у тебя экшены не связаны с формой, смело вызывай actionFunction() внутри onClick.

Итоги

useActionState - «прокачанный» вариант useState, позволяющий удобно обрабатывать одно «действие» (будь то отправка формы, добавление в корзину или загрузка файла) и иметь встроенный флаг загрузки. Он идеально подходит для сценариев, где после нажатия на кнопку (или отправки формы) нужно дождаться ответа с сервера и показать, как всё прошло: успешно или с ошибкой.

  • Не для всего: если у тебя сложная форма с десятком полей и множеством отдельных действий, useActionState не заменит useReducer или несколько useState.

  • Для «быстрой магии»: когда тебе нужно всего лишь одно действие, результат и isPending, он сэкономит время и строки кода.

  • Интеграция с Server Components: даёт возможность обрабатывать «серверные действия» до гидратации, что ускоряет UX и упрощает код.

Если ты искал способ упростить логику «запрос — результат — отображение» и при этом не хотел городить огород изuseState, useActionState может стать тем самым «Ах, вот оно!».

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


  1. markelov69
    27.12.2024 09:03

    Если ты искал способ упростить логику «запрос — результат — отображение» и при этом не хотел городить огород изuseStateuseActionState может стать тем самым «Ах, вот оно!».

    С 2017 года можно все делать намного лучше и проще, а главное супер гибко просто тупо используя MobX в связке с React'ом.
    React по сей день в плане управлением состоянием это просто ничтожество. Смотреть на эти жалкие попытки добавления новых функций смешно)
    Для тех кто использует MobX нет разницы какой версии react использовать 16,17,18,19.


    1. andry36 Автор
      27.12.2024 09:03

      Спасибо за комментарий!
      Конечно, у MobX есть свои неоспоримые преимущества - он удобен для организации гибкого и реактивного стейт-менеджмента, особенно когда в приложении много взаимосвязанных состояний и сложная бизнес-логика. Однако у каждой задачи свои масштабы и особенности.

      • Локальные экшены и формы. Когда речь идёт об обработке единичного действия (отправка формы, загрузка файла и т. п.), не всегда хочется поднимать глобальное хранилище, прописывать сторы, экшены и т. д. проще зацепить локальный хук и закрыть задачу за пару строк. В таких случаях удобнее иметь что-то вроде useActionState, которое интегрировано в экосистему React.

      • Server Components призваны минимизировать клиентскую часть и ускорить загрузку. Некоторые фреймворки (например, Next.js) поддерживают это «из коробки». useActionState встроен в эту концепцию, позволяя обрабатывать действия еще до гидратации.

      • Никто не заставляет бросать MobX - в крупных проектах, где уже есть MobX, Redux, Zustand и пр., можно совмещать их с хуками. Всё зависит от того, какой подход более оптимален в конкретном месте приложения.

      Так что выбор инструмента всегда сводится к балансированию между гибкостью, скоростью разработки и сложностью задачи. MobX отлично покрывает определённые сценарии, React-хуки - другие. Здорово, что у нас теперь есть несколько инструментов на выбор, а не «один-единственный фреймворк на все случаи жизни».


      1. clerik_r
        27.12.2024 09:03

        не всегда хочется поднимать глобальное хранилище

        Зачем? Если нужно локальное, просто используйте локальное. Вот так:

        const MyCompoent = observer((props: IMyCompoentProps) => {
          const [state] = useState(() => MyCompoentLocalState(props));
          state.props = props;
        
          return <div>123</div>
        });

        И все дела.

        прописывать сторы, экшены и т. д.

        Зачем эта грязь? Все автоматом делается.

        class MyCompoentLocalState {
          props: IMyCompoentProps = null; 
          
          constructor(props) {
            makeAutoObservable(this);
            this.props = props;
          }
          
          someFunction = async () => {
            //...
          }
        
          someFunction2 = () => {
            //...
          }
        }

        И все дела. Все максимально чисто и предельно структурировано и понятно, всем и всегда. Т.к. по сути это тупо нативный код.

        Server Components призваны минимизировать клиентскую часть и ускорить загрузку.

        Актуально исключительно в проектах где нужно SEO и то, только для страниц нужных для SEO. В остальных случаях бесполезно и пагубно влияет на производительность, особенно в плане нагрузки на сервера, они стоят денег. Плюс всё что закрыто авторизацией так же не нуждается в серверном рендеринге и SEO.