useActionState: новый герой в мире React
- суббота, 28 декабря 2024 г. в 00:00:09
Привет, бравый покоритель фронтенда! Если ты когда-либо ковырялся в 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
- замечательный хук, и он с лёгкостью покрывает 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 жил в 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
- булевый флаг, показывающий, что файл загружается, и отключающий кнопку, чтобы избежать повторных кликов во время процесса.
Одна из важных «фишек» 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 сопоставит обновлённое состояние с интерфейсом, так что пользователю всё равно всё будет работать «из коробки».
Инициализируй состояние так, чтобы оно отражало реальный объект, с которым предстоит работать.
Обрабатывай ошибки возвращая из своей функции объект с error
или success
, чтобы в интерфейсе легко было показать соответствующее сообщение.
Пользуйся isPending
. Не бойся показывать индикатор загрузки или дизейблить кнопку. Пользователи любят понимать, что что-то происходит.
Не ограничивай себя формами. Если у тебя экшены не связаны с формой, смело вызывай actionFunction()
внутри onClick
.
useActionState - «прокачанный» вариант useState
, позволяющий удобно обрабатывать одно «действие» (будь то отправка формы, добавление в корзину или загрузка файла) и иметь встроенный флаг загрузки. Он идеально подходит для сценариев, где после нажатия на кнопку (или отправки формы) нужно дождаться ответа с сервера и показать, как всё прошло: успешно или с ошибкой.
Не для всего: если у тебя сложная форма с десятком полей и множеством отдельных действий, useActionState не заменит useReducer
или несколько useState
.
Для «быстрой магии»: когда тебе нужно всего лишь одно действие, результат и isPending
, он сэкономит время и строки кода.
Интеграция с Server Components: даёт возможность обрабатывать «серверные действия» до гидратации, что ускоряет UX и упрощает код.
Если ты искал способ упростить логику «запрос — результат — отображение» и при этом не хотел городить огород изuseState
, useActionState может стать тем самым «Ах, вот оно!».