DevGang
Авторизоваться

Новый хук React хук useFormStatus(): изменяем способ использования форм

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

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

Что-то вроде того, что вы видите на гифке ниже:

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

Вот почему мне нравится новый хук useFormStatus(): он упрощает создание состояний загрузки в формах и делает весь процесс автоматическим. Читайте дальше, чтобы узнать, как использовать новый хук useFormStatus() в вашем проекте React.

Проблема

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

Этот компонент TodoList отображает форму, содержащую ввод, кнопку отправки и список элементов задач, которые в данный момент хранятся в локальном состоянии:

import { FormEvent, useRef, useState } from "react";

export default function TodoList() {
  const [todos, setTodos] = useState([]);
  const inputRef = useRef(null);

  async function onSubmit(e) {
    e.preventDefault();

    if (inputRef.current == null) return;

    const newTodo = await createTodo(inputRef.current.value);
    setTodos((prev) => [...prev, newTodo]);
  }

  return (
    <>
      <form onSubmit={onSubmit}>
        <label>Title</label>
        <br />
        <input ref={inputRef} required />
        <br />
        <button type="submit">Create</button>
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      </form>
    </>
  );
}

function createTodo(title) {
  return wait({ id: crypto.randomUUID, title }, 1000);
}

function wait(value, duration) {
  return new Promise((resolve) => setTimeout(() => resolve(value), duration));

Когда вы нажимаете Add Item, чтобы отправить форму, вы также вызываете функцию onSubmit(). Затем эта функция вызывает функцию createTodo(), возвращает элемент после задержки в 1 секунду и сохраняет его в локальном состоянии как initialTodos.

В этом случае вызов функции createTodos() заменяет вызов API, что вы и сделали бы в реальном сценарии.

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

В результате пользователь может просто спамить кнопку Add Item несколько раз. Это отправит несколько запросов и добавит одни и те же задачи в список на странице (что изначально не входило в их намерения).

Это распространенная проблема, если приложение работает при медленном сетевом соединении.

Во-первых, мы начнем с решения этой проблемы традиционным методом. После этого мы представим новый хук useFormStatus(), чтобы еще больше упростить решение.

Традиционное решение

Обычный способ справиться с этим — создать переменную состояния isLoading, для которой мы установим значение true непосредственно перед выполнением любого запроса API и значение false, как только получим ответ.

Затем мы должны использовать это состояние isLoading на странице, чтобы уведомить пользователя о статусе запроса API (например, отключив кнопку и показывая текст «Loading…» до тех пор, пока элемент задачи не будет добавлен).

Вот обновленный код:

// Imports 

export function TodoList() {
  const [isLoading, setIsLoading] = useState(false)

  async function onSubmit(e: FormEvent) {
    // Set loading to true before request
    setIsLoading(true)
    const newTodo = await createTodo(inputRef.current.value)
    // Set back to "false" after request
    setIsLoading(false)

    setTodos(prev => [...prev, newTodo])
  }

  return (
    <>
      <form onSubmit={onSubmit}>
        // other JSX elements...
        <button type="submit" disabled={isLoading}>
          { isLoading? "Loading...": "Add Item"}
        </button>
       // other JSX elements...
      </form>
    </>
  )
}

Хотя это отличное решение, оно требует создания дополнительной переменной isLoading. Если у вас много разных форм, у вас может быть много состояний загрузки, и может быть сложно правильно их назвать.

Это просто не идеально. Вот тут-то и пригодится хук useFormStatus().

Решение useFormStatus()

Прежде чем мы начнем, обратите внимание, что useFormStatus() — это экспериментальный хук, которого нет в стабильной версии React. Поэтому, чтобы использовать это, вам необходимо убедиться, что вы используете экспериментальную версию React и ReactDOM.

Теперь импортируйте хук:

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

Затем вам нужно подключить его к пользовательскому компоненту вашей формы. Другими словами, вам нужно иметь собственный компонент в вашей форме, а затем использовать внутри него хук useFormStatus.

Итак, под компонентом TodoList создайте собственный компонент SubmitButton и скопируйте в него кнопку из формы:

function SubmitButton() {
  const data = useFormStatus()
  const isLoading = data.pending

  return (
    <button type="submit" disabled={isLoading}>
      { isLoading? "Loading...": "Create"}
    </button>
  )
}

Затем внутри компонента Form замените элемент кнопки компонентом SubmitButton:

<SubmitButton />

Кроме того, удалите все вхождения isLoading внутри компонента формы, поскольку вы будете обрабатывать все с помощью перехватчика useFormStatus внутри вашего компонента SubmitButton.

Затем измените свойство формы с onSubmit на action:

<form action={onSubmit}>
  // Other markup
</form>

Вместо того, чтобы принимать объект события в качестве аргумента в функции onSubmit, вместо этого примите FormData (именно это передает действие). Это обновленная версия onSubmit:

function onSubmit(data) {
  const title = data.getTitle;

  if(typeof title === "string") return 

  const newTodo = await createTodo(title)
  // Set back to "false" after request
  setTodos(prev => [...prev, newTodo])
}

Пока мы ждем разрешения обещания, для этого свойства data.pending будет установлено значение true. Таким образом, кнопка будет отключена, и во время этого процесса будет отображаться текст загрузки.

Это полный исходный код:

import {FormEvent, useRef, useState } from 'react'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'  

export function TodoList() {
  const [todos, setTodos] = useState(initialTodos)
  const inputRef = useRef(null)

  async function onSubmit(data) {
    const title = data.getTitle;

    if(typeof title === "string") return 

    const newTodo = await createTodo(title)

    setTodos(prev => [...prev, newTodo])
  }

  return (
    <>
      <form onSubmit={onSubmit}>
        <label>Title</label>
        <br />
        <input ref={inputRef} required/>
        <br />
        <SubmitButton />
        <ul>
        {
          todos.map(todo => {
            <li key={todo.id}>{todo.title}</li>
          })
        }
        </ul>
      </form>
    </>
  )
}

function SubmitButton() {
  const data = useFormStatus()
  const isLoading = data.pending

  return (
    <button type="submit" disabled={isLoading}>
      { isLoading? "Loading...": "Create"}
    </button>
  )
}

function createTodo(title) {
  return wait({id: crypto.randomUUID, title}, 1000)
}

function wait(value, duration) {
  return new Promise(resolve => 
    setTimeout(() => resolve(value), duration)
  )
}

Заключение

Решение useFormStatus() может показаться не намного лучше традиционного решения с точки зрения объема кода, который вам придется написать.

Но если вы работаете с чем-то вроде Next.js или чем-то еще, что уже реализует FormData, использовать эту систему намного проще и устраняется необходимость писать весь дополнительный шаблонный код.

Источник:

#JavaScript #React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу