Новый хук 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
, использовать эту систему намного проще и устраняется необходимость писать весь дополнительный шаблонный код.