Optimistic UI с useFetcher

Optimistic UI — это подход к построению пользовательского интерфейса, при котором изменения в интерфейсе отображаются сразу, ещё до подтверждения операции сервером. Такой подход создаёт ощущение мгновенной реакции приложения, улучшая пользовательский опыт.

В контексте React Router ключевую роль в реализации Optimistic UI играет хук useFetcher. Он позволяет отправлять запросы на сервер асинхронно и обновлять состояние интерфейса без полной перезагрузки страницы или перехода по маршруту.


Хук useFetcher

useFetcher предоставляет объект, через который можно отправлять действия (action) и запросы (loader) для конкретного маршрута, независимо от текущего маршрута. Основные свойства и методы, которые он предоставляет:

  • fetcher.submit(data, options) — отправка данных на сервер. data может быть объектом FormData или простым объектом, а options определяют метод (GET/POST) и путь маршрута.
  • fetcher.load(url) — загрузка данных с указанного маршрута без навигации.
  • fetcher.data — данные, возвращённые сервером.
  • fetcher.state — текущее состояние запроса: "idle", "submitting", "loading".
  • fetcher.formMethod, fetcher.formAction — свойства, доступные при использовании формы.

Принцип работы Optimistic UI

  1. Пользователь инициирует действие (например, добавление комментария).
  2. Сразу же отображается обновление интерфейса, как если бы сервер уже подтвердил операцию.
  3. Одновременно выполняется запрос на сервер через fetcher.submit.
  4. После получения ответа можно скорректировать интерфейс, если сервер вернул ошибку или изменения отличаются от оптимистичного ожидания.

Пример реализации добавления комментария

import { useFetcher } from "react-router-dom";
import { useState } from "react";

function CommentForm({ postId }) {
  const fetcher = useFetcher();
  const [optimisticComments, setOptimisticComments] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const commentText = formData.get("comment");

    // Добавление оптимистичного комментария
    const tempComment = {
      id: `temp-${Date.now()}`,
      text: commentText,
      pending: true
    };
    setOptimisticComments([...optimisticComments, tempComment]);

    // Отправка на сервер
    fetcher.submit(formData, { method: "post", action: `/posts/${postId}/comments` });

    // Очистка формы
    e.target.reset();
  };

  // Обновление списка после ответа сервера
  const comments = fetcher.data?.comments ?? [];
  const combinedComments = [...optimisticComments.filter(c => c.pending), ...comments];

  return (
    <fetcher.Form onSub mit={handleSubmit}>
      <input name="comment" type="text" required />
      <button type="submit" disabled={fetcher.state === "submitting"}>Добавить</button>
      <ul>
        {combinedComments.map(c => (
          <li key={c.id}>{c.text} {c.pending && "(отправляется…)"}</li>
        ))}
      </ul>
    </fetcher.Form>
  );
}

Разбор ключевых моментов:

  • optimisticComments хранит локальные изменения до подтверждения сервера.
  • При отправке формы создаётся временный комментарий с флагом pending.
  • fetcher.submit выполняет POST-запрос на сервер.
  • После получения ответа серверные данные объединяются с оптимистичными для корректного отображения.

Работа с состояниями fetcher.state

Хук useFetcher автоматически обновляет состояние:

  • "idle" — нет активного запроса.
  • "submitting" — данные отправляются на сервер.
  • "loading" — загрузка данных с маршрута или обновление после действия.

Для Optimistic UI важно реагировать на "submitting" для отображения индикаторов и предотвращения повторных запросов.

<button type="submit" disabled={fetcher.state === "submitting"}>
  {fetcher.state === "submitting" ? "Отправка..." : "Добавить"}
</button>

Обработка ошибок сервера

Если сервер вернёт ошибку, оптимистичный комментарий следует удалить или отметить ошибкой:

useEffect(() => {
  if (fetcher.data?.error) {
    setOptimisticComments(prev =>
      prev.map(c => c.pending ? { ...c, error: fetcher.data.error } : c)
    );
  } else {
    // Удаляем флаг pending для подтверждённых комментариев
    setOptimisticComments(prev =>
      prev.map(c => ({ ...c, pending: false }))
    );
  }
}, [fetcher.data]);

Советы по использованию Optimistic UI

  • Не злоупотреблять оптимизмом: для критичных данных лучше дождаться ответа сервера.
  • Идентификация временных элементов: временные ID (temp-${Date.now()}) позволяют корректно обновлять и удалять элементы после ответа.
  • Синхронизация с сервером: после ответа всегда проверять корректность состояния интерфейса.
  • Использование fetcher.load для обновления данных: позволяет подтягивать актуальные данные после действий без навигации.

useFetcher в связке с Optimistic UI позволяет создавать интерфейсы, которые реагируют мгновенно, не блокируют пользователя и поддерживают согласованность с сервером. Такой подход особенно эффективен для форм, списков комментариев, лайков и других интерактивных элементов, где важна скорость отклика.