useActionState: новый герой в мире React

от автора

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 может стать тем самым «Ах, вот оно!».


ссылка на оригинал статьи https://habr.com/ru/articles/870216/