Как настроить optimistic UI в React

Вопрос: как настроить optimistic UI при отправке формы в React?

Линия поддержки
Линия поддержки Ответы на вопросы пользователей
21 октября 2025

Как реализовать обновление так, чтобы можно было откатить состояние и избежать рассинхронизации данных между интерфейсом и сервером?

Изображение записи

Комментарий пользователя

Здравствуйте! Хочу, чтобы интерфейс мгновенно показывал результат отправки (например, когда добавляю комментарий), а потом синхронизировал с сервером. Как реализовать обновление так, чтобы при ошибке откатывать состояние и не допустить рассинхронизации?

Петр Семенов Пользователь

Ответ специалиста

Здравствуйте, Петр! Optimistic UI делает интерфейс быстрее и отзывчивее: сразу обновляет локальное состояние. Он показывает новый элемент, еще до ответа сервера. А если сервер потом вернет ошибку, интерфейс либо откатится назад, либо обновит данные реальными значениями от сервера.

В коде ниже показан импорт и вспомогательная функция API для имитации запроса — в реальном проекте их нужно заменить на fetch или axios:

  • Полина Цуканова

    Полина Цуканова

    Фронтенд-разработчик


      import React, { useState, useCallback } from 'react';

// Симуляция POST /comments: задержка + 15% шанс ошибки
export async function postCommentApi(body) {
  // В реальном коде: return fetch('/api/comments', { method: 'POST', body: JSON.stringify(body) }).then(r => r.json())
  await new Promise(r => setTimeout(r, 700));
  if (Math.random() < 0.15) throw new Error('Server error');
  return { id: Date.now(), ...body }; // сервер вернет реальный id
}

Компонент списка комментариев. Временные комментарии нужно визуально выделять — например, снижать непрозрачность и показывать специальный текст, чтобы пользователь видел, что это временный элемент. Код должен быть внутри React-компонента:


      import React, { useState, useCallback } from 'react';
import { postCommentApi } from './api'; // путь к блоку с API
import CommentForm from './CommentForm'; // путь к блоку с формой

export default function Comments() {
  const [comments, setComments] = useState([]);
  const [pending, setPending] = useState(false);

  const addComment = useCallback(async (text) => {
    const tempId = `tmp_${Date.now()}`;
    const temp = { id: tempId, text, status: 'pending' };

    // оптимистично показываем комментарий в UI
    setComments(prev => [temp, ...prev]);
    setPending(true);

    try {
      const saved = await postCommentApi({ text });
      // заменяем временный элемент на реальный ответ от сервера
      setComments(prev => prev.map(c => c.id === tempId ? { ...saved, status: 'saved' } : c));
    } catch (err) {
      // при ошибке откатываем — удаляем временный элемент
      setComments(prev => prev.filter(c => c.id !== tempId));
      console.error('Failed to save comment:', err);
      // здесь можно показать toast/alert с ошибкой
    } finally {
      setPending(false);
    }
  }, []);

  return (
    <div>
      <CommentForm onSubmit={addComment} disabled={pending} />
      <ul>
        {comments.map(c => (
          <li key={c.id} style={{ opacity: c.status === 'pending' ? 0.6 : 1 }}>
            {c.text} {c.status === 'pending' ? '(sending...)' : ''}
          </li>
        ))}
      </ul>
    </div>
  );
}

В компонент списка комментариев должен быть импортирован компонент формы CommentForm использует state для ввода:


      import React, { useState } from 'react';

export default function CommentForm({ onSubmit, disabled }) {
  const [text, setText] = useState('');
  const handle = async (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    await onSubmit(text.trim());
    setText('');
  };
  return (
    <form onSubmit={handle}>
      <input value={text} onChange={e => setText(e.target.value)} disabled={disabled} />
      <button type="submit" disabled={disabled || !text.trim()}>Send</button>
    </form>
  );
}

Обратите внимание: добавление комментария должно происходить мгновенно. После этого либо появится сохраненный комментарий с реальным ID, либо временный исчезнет, если произошла ошибка, а пользователю покажется соответствующее сообщение.

Не создавайте ключи элементов прямо в render, например с помощью Math.random(). Временный ID нужно генерировать один раз при создании объекта, чтобы ключ оставался стабильным до окончания операции.

Больше полезных материалов и новости из мира IT — в рассылке Академии Selectel.