javascript

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

  • суббота, 28 декабря 2024 г. в 00:00:09
https://habr.com/ru/articles/870216/
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 может стать тем самым «Ах, вот оно!».