Кнопка с загрузкой — React, Promise и поток обновлений
См. также: Первая программа на React · Асинхронность · Генераторы · Утилита wait · Кнопка "Поделиться" (vanilla) · Web API на практике - примеры кода
Задача
Пользователь нажимает кнопку "Создать заметку" или "Отправить". Пока сервер отвечает, нужно:
- Заблокировать повторный клик — иначе уйдут два одинаковых запроса.
- Показать, что идёт работа — текст
"...", спиннер или пошаговые сообщения. - Дождаться Promise —
fetch,navigator.shareили любаяasync-функция.
В первой программе на React загрузка списка уже есть через useEffect. Здесь другой сценарий: действие по клику (onClick), а не автоматический запрос при монтировании.
Ниже три уровня — от ручного isLoading до потока значений из async function*.
Плавный старт — как воспринимать паттерн
Кнопка с загрузкой — это маленькая state-machine:
idle— кнопка активна;loading— действие выполняется и повторный запуск блокируется;done/error— интерфейс возвращается в рабочее состояние.
Такой взгляд помогает писать надёжный UI без случайных дублей запросов и зависших disabled-состояний.
Мини-кейс "до/после"
| Подход | Поведение |
|---|---|
| До | двойной клик отправляет два POST, пользователь не понимает, что происходит |
| После | один клик запускает одну операцию, статус виден прямо на кнопке |
Базовый паттерн — useState и isLoading
Состояние загрузки хранят в useState. Пока операция выполняется, кнопку отключают (disabled) и меняют подпись (label).
import { useState } from 'react';
async function createTodoAPI(text) {
const res = await fetch('http://127.0.0.1:3000/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
function CreateTodoButton() {
const [isLoading, setIsLoading] = useState(false);
async function handleClick() {
if (isLoading) return;
setIsLoading(true);
try {
await createTodoAPI('new todo text');
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
}
return (
<button type="button" onClick={handleClick} disabled={isLoading}>
{isLoading ? '...' : 'Create Todo'}
</button>
);
}
| Элемент | Роль |
|---|---|
useState(false) | флаг "операция в процессе" |
if (isLoading) return | защита от двойного клика до перерисовки |
disabled={isLoading} | браузер не шлёт повторный click |
isLoading ? '...' : 'Create Todo' | смена label на время ожидания |
try/finally | isLoading сбрасывается и при ошибке |
onClick={handleClick} — передаётся ссылка на функцию. Не пишите onClick={handleClick()}: тогда функция вызовется сразу при рендере. Подробнее — в частых ошибках React.
Promise и Event Loop — в асинхронности.
Обёртка LoadingButton
Один и тот же шаблон (isLoading, disabled, текст) повторяется в каждой форме. Его выносят в переиспользуемый компонент:
import { useState } from 'react';
function LoadingButton({ onClick, children }) {
const [isLoading, setIsLoading] = useState(false);
async function handleClick() {
if (isLoading) return;
setIsLoading(true);
try {
await onClick();
} finally {
setIsLoading(false);
}
}
return (
<button type="button" onClick={handleClick} disabled={isLoading}>
{isLoading ? '...' : children}
</button>
);
}
function CreateTodoButton() {
return (
<LoadingButton
onClick={async () => {
await createTodoAPI('new todo text');
}}
>
Create Todo
</LoadingButton>
);
}
| Prop | Смысл |
|---|---|
children | обычный label кнопки ("Create Todo", "Сохранить") |
onClick | функция, возвращающая Promise (часто async () => { ... }) |
Компонент не знает, что именно делает onClick — только ждёт завершения Promise и управляет UI.
Сокращение через useAsyncCallback (библиотека)
Пакет react-async-hook инкапсулирует тот же паттерн:
import { useAsyncCallback } from 'react-async-hook';
function AppButton({ onClick, children }) {
const asyncOnClick = useAsyncCallback(onClick);
return (
<button
type="button"
onClick={asyncOnClick.execute}
disabled={asyncOnClick.loading}
>
{asyncOnClick.loading ? '...' : children}
</button>
);
}
| Поле | Роль |
|---|---|
execute | запуск обработчика по клику |
loading | аналог isLoading |
Это внешняя зависимость (npm install react-async-hook). Для учебных проектов достаточно своего LoadingButton на useState; библиотека удобна, когда таких кнопок много и нужны счётчики ошибок или отмена.
Поток значений — async function* и yield
Иногда одного "..." мало: длинная операция и пользователь должен видеть этапы — "Отправка", "Ожидание сервера", "Готово". Тогда обработчик делают асинхронным генератором: async function* () { yield ...; await ...; }.
Каждый yield отдаёт следующее значение label (строка или JSX). Между шагами — await wait(ms) (утилита из 32.md) или реальный fetch.
import { useState } from 'react';
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function LoadingButton({ onClick, children, className }) {
const [label, setLabel] = useState(children);
const [isLoading, setIsLoading] = useState(false);
async function handleClick() {
if (isLoading) return;
setIsLoading(true);
setLabel(children);
try {
const result = onClick();
if (result && typeof result[Symbol.asyncIterator] === 'function') {
for await (const step of result) {
setLabel(step);
}
} else {
await result;
}
} finally {
setIsLoading(false);
setLabel(children);
}
}
return (
<button
type="button"
className={className}
onClick={handleClick}
disabled={isLoading}
>
{isLoading ? label : children}
</button>
);
}
function Demo() {
return (
<LoadingButton
className="counter"
onClick={async function* () {
yield 'Starting some process';
await wait(1000);
yield 'Oops, taking longer than usual';
await wait(2000);
yield (
<span>
Maybe there is a <code>Retry-after</code>?
</span>
);
await wait(3000);
yield 'Nevermind, I got it';
}}
>
Click me
</LoadingButton>
);
}
Схема одного клика:
| Конструкция | Связь с другими темами |
|---|---|
async function* | асинхронные генераторы |
for await...of | обход AsyncIterator — потока значений |
yield | пауза генератора и отдача очередного label |
wait(ms) | Promise вокруг setTimeout — 32.md |
Теория генераторов и fetchPages — в 22.md; здесь тот же механизм, но значения идут в текст кнопки, а не в консоль.
label, children и JSX внутри кнопки
children— подпись в спокойном состоянии ("Click me", "Сохранить").- Во время загрузки
label(state) может быть строкой или JSX — React перерисует содержимое<button>. - После
finallyподпись возвращают кchildren, чтобы кнопка снова была узнаваемой.
Если этапы только текстовые, достаточно строк в yield. JSX уместен, когда на кнопке нужен <code> или короткая разметка без отдельного модального окна.
Сравнение подходов
| Подход | Когда использовать |
|---|---|
isLoading вручную | одна-две кнопки, учебный минимум |
<LoadingButton onClick={async () => ...}> | формы, мутации, POST через Node API |
useAsyncCallback | много async-кнопок, нужен готовый хук |
async function* + yield | длинные операции, пошаговый feedback на самой кнопке |
Загрузка списка при открытии страницы по-прежнему через useEffect — см. 272.md. Кнопка с isLoading — для действий пользователя.
Частые ошибки
| Симптом | Причина |
|---|---|
| Два одинаковых POST | нет disabled / isLoading, двойной клик |
| Кнопка "вечно" disabled | забыли setIsLoading(false) в finally |
| Запрос при загрузке страницы | onClick={save()} вместо onClick={() => save()} |
| Генератор не обновляет текст | не async function*, а обычная async function |
yield без await между шагами | UI не успевает перерисоваться — добавьте wait(0) или реальный I/O |
Связь с другими темами раздела
| Тема | Где углубиться |
|---|---|
useState, компоненты, onClick | 272.md, 27.md |
Promise, async/await | 21.md |
function*, async function*, for await | 22.md |
wait(ms) | 32.md |
| Async по клику без React | 44.md |
| POST к API | 262.md · 264.md |
Практика для продуктовых интерфейсов
Кнопка с загрузкой в реальном проекте обычно делает больше, чем смена подписи:
- показывает прогресс или этап операции;
- блокирует соседние кнопки, если операция конфликтует;
- отдаёт понятное сообщение об ошибке рядом с формой;
- после успеха обновляет список и возвращает фокус пользователю.
Такой подход повышает предсказуемость интерфейса и снижает число повторных кликов.
Чек-лист качества для LoadingButton
| Проверка | Ожидаемое поведение |
|---|---|
| Двойной клик | второй запуск блокируется |
| Ошибка API | isLoading сбрасывается в finally |
| Медленный запрос | подпись/индикатор сообщают о процессе |
| Успешный ответ | UI обновляется без перезагрузки страницы |
| Отмена операции | состояние кнопки корректно возвращается |
Для сетевых операций соединяйте этот паттерн с AbortController и валидацией форм, чтобы получить полный цикл "проверка → отправка → отклик интерфейса".
Краткий итог
useState(false)+isLoading— флаг загрузки;disabled={isLoading}блокирует повторный клик.onClickсasyncиawaitждёт Promise (сеть, таймер, API).LoadingButtonвыносит повторяющийся шаблон;children— обычный label.async function*иyieldдают поток значений для пошагового текста на кнопке;for await...ofобходит этот поток в React.wait(ms)связывает паузы с Promise — удобно междуyieldв учебных примерах.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Основы JavaScript - стандарт ECMAScript, модель выполнения и базовые конструкции языка. JavaScript — это язык программирования, который позволяет создавать интерактивные веб-страницы, серверные приложения и мобильные программы. Для создания массивов используется литеральная нотация. Конструктор Array не применяется. Как работать с HTML-элементами, как их создавать, менять. Простые приложения на JavaScript - базовые сценарии, структура кода и быстрый старт с практическими примерами. Расширения файлов определяют способ обработки кода средой выполнения или компилятором. История JavaScript - происхождение языка, ключевые этапы развития и влияние на современный веб. Такое именование представляет собой соглашение между разработчиками. Классический JavaScript не обеспечивает реальной приватности через подчеркивания. JavaScript содержит набор зарезервированных слов, которые имеют специальное значение в языке. Эти слова нельзя использовать в качестве идентификаторов для переменных, функций или классов. Встроенные функции JavaScript - ключевые методы массивов, строк и объектов для повседневной разработки. Этот шаблон описывает подключение внешних функций, классов или значений из других файлов. Он используется в начале файла и определяет зависимости текущего модуля. JavaScript используется для создания кроссплатформенных мобильных приложений, которые работают на iOS и Android с использованием единой кодовой базы.Основы JavaScript
Что требуется знать перед началом изучения языка программирования JavaScript
Рекомендации по разработке на JavaScript
Работа с HTML в JavaScript
Простые приложения на JavaScript
Форматы JavaScript
История языка JavaScript
Синтаксис и пунктуация в JavaScript
Ключевые слова языка JavaScript
Встроенные функции JavaScript
Структура и подключение JavaScript-кода
Применение JavaScript в вебе и за его пределами