awesome-everything EN
↑ Обратно к восхождению

Очереди, потоки, события

UX при eventual consistency: чтение кода

Суть Читай реальные React/TanStack Query сниппеты над асинхронным бэкендом, предсказывай UX-баг и выбирай фикс с наибольшим рычагом, который senior сделает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Баги eventual consistency прячутся в обработчике мутации и в пути retry. Прочитай код, предскажи, что увидит пользователь, когда consumer отстаёт или запрос падает, затем выбери фикс, который senior-инженер сделает первым.

Цель

Отработай цикл, который ты запускаешь в каждом ревью async-UX: прочитай мутацию, предскажи расхождение между optimistic-состоянием и серверной правдой и потянись за фиксом, завершающим контракт — снапшот, rollback, reconcile, key — раньше, чем добавишь спиннеры.

Сниппет 1 — недоделанное optimistic-обновление

const mutation = useMutation({
  mutationFn: (todo) => api.addTodo(todo),
  onMutate: async (todo) => {
    await qc.cancelQueries({ queryKey: ['todos'] });
    qc.setQueryData(['todos'], (old) => [...old, todo]);
    // ничего не возвращает
  },
  onSettled: () => qc.invalidateQueries({ queryKey: ['todos'] }),
});
Викторина

Запрос иногда падает (consumer отклоняет запись). Что увидит пользователь и какой единственный фикс с наибольшим рычагом?

Сниппет 2 — кнопка retry

function PayButton({ orderId, amount }) {
  const [status, setStatus] = useState('idle');
  async function pay() {
    setStatus('pending');
    await fetch('/api/charge', {
      method: 'POST',
      body: JSON.stringify({ orderId, amount }),
    });
    setStatus('done');
  }
  // пользователь не видит прогресса, жмёт pay() снова на медленной сети
  return <button onClick={pay}>Pay</button>;
}
Викторина

На медленной сети пользователь кликает дважды. Что идёт не так и какой верный фикс?

Сниппет 3 — тост ложного успеха

async function publish(post) {
  const res = await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify(post),
  });
  if (res.status === 202) {
    toast.success('Published!');
    await qc.invalidateQueries({ queryKey: ['posts'] });
  }
}
Викторина

POST возвращает 202, а consumer пишет пост ~800мс спустя. Что здесь не так — две вещи?

Сниппет 4 — конфликт, который перезаписывает

function onIncomingUpdate(remote) {
  // у local есть несохранённые правки; remote пришёл от другого клиента
  setDoc(remote); // last write wins, молча
}
Викторина

Два клиента правят тело одного документа. Этот обработчик запускается на входящем remote-обновлении. В чём senior-возражение и какая форма лучше?

Итог

Каждый async-UX баг читается в мутации и в пути retry: optimistic-обновление без пары снапшот-и-onError-rollback оставляет упавшую запись на экране; POST без idempotency key превращает двойной клик в двойное списание; сворачивание 202 в success-тост плюс мгновенный рефетч — это ложный успех в гонке с consumer’ом; а тихий обработчик last-write-wins уничтожает несохранённые правки пользователя. Завершай контракт apply-send-reconcile, ключуй retry, показывай честные pending-состояния и разрешай конфликты осознанно, а не перезаписью.

Продолжить восхождение ↑UX при eventual consistency: сделай асинхронный бэкенд мгновенным
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.