Intermediate · Connecting frontend and backend · topic 20 of 34 · 30 min
Keeping the screen in sync
Explanation
The screen can go out of date
The notes list in React state is a copy of what the server has. When the user creates, edits or deletes a note, the server changes, but the copy does not change by itself. You have three ways to bring them back in line.
| Way | How | Good | Not so good |
|---|---|---|---|
| Refetch | After the change, GET /api/notes again | Simple and always correct | One more request; a short wait |
| Update from the answer | Put the note the server returned into the local list | No extra request | You must write the add, edit and delete logic |
| Optimistic update | Change the screen first, then send the request; undo if it fails | Feels instant, even on slow networks | Needs rollback code; wrong for risky actions |
// after POST answered 201 with the new note
setNotes((list) => [created, ...list]);
// after PATCH answered 200 with the updated note
setNotes((list) => list.map((n) => (n.id === updated.id ? updated : n)));
// after DELETE answered 204
setNotes((list) => list.filter((n) => n.id !== id));Optimistic updates
An optimistic update assumes the request will work. When the user taps the pin icon, the note moves to the top at once; the PATCH request goes in the background. If the server fails, you roll back: put the old list back and show a short message.
async function togglePin(id) {
const before = notes; // keep the old list for rollback
const target = notes.find((n) => n.id === id);
setNotes(notes.map((n) => (n.id === id ? { ...n, pinned: !n.pinned } : n)));
try {
await api('/notes/' + id, { method: 'PATCH', body: { pinned: !target.pinned } });
} catch (err) {
setNotes(before); // roll back
showToast('Could not pin the note. Please try again.');
}
}Server-state libraries
Server state is data that really lives on the server, which the screen only shows a copy of. Libraries such as TanStack Query (also called React Query) and SWR manage that copy for you: they cache answers by a key like ["notes"], show cached data at once while fetching fresh data in the background, refetch when you come back to the tab, retry failed reads, and let you invalidate (mark as old) a key after a change so it is fetched again.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function NotesPage() {
const queryClient = useQueryClient();
const { data, isPending, error } = useQuery({
queryKey: ['notes'],
queryFn: () => api('/notes'),
});
const createNote = useMutation({
mutationFn: (input) => api('/notes', { method: 'POST', body: input }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['notes'] }),
});
// createNote.mutate({ title: 'New idea' })
}Race conditions in search
A race condition is a bug where the result depends on which of two things finishes first. The user types "ph", then "phy". Two searches go out. If the "ph" answer is slower and arrives last, the screen shows results for "ph" while the box says "phy". The fix: give each request a number and ignore any answer that is not from the latest one, or cancel the older request with an AbortController.
let latest = 0;
async function search(text) {
const id = ++latest; // this request's number
const data = await api('/notes?q=' + encodeURIComponent(text));
if (id !== latest) return; // a newer search started: ignore this old answer
setNotes(data.notes);
}Debounce typing
Typing "physics" is seven key presses. Sending seven searches wastes data and server time. Debounce means waiting until the user stops typing for a short pause, about 300 ms, and only then sending one request.
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer); // a new key press cancels the waiting call
timer = setTimeout(() => fn(...args), ms);
};
}
const debouncedSearch = debounce(search, 300);
// <input onChange={(e) => debouncedSearch(e.target.value)} />Watch the lesson
Keeping the screen in sync · 1/7
When a note is created, edited or deleted, the list on screen must match the server again.
Prefer a video by a teacher? Find free videos on “react optimistic updates tanstack query invalidate race condition debounce search” on YouTube ↗
In real life: Liking a post in a social app
In most social and chat apps, when you tap the heart on a post it turns red at once and the count goes up, even on a slow 4G connection. The request goes in the background. If it fails, the heart quietly turns grey again. The app search box works the same careful way: it waits until you stop typing, and it ignores answers to words you have already changed. A food delivery app, on the other hand, never shows "Order placed" until the server confirms, because a false success there costs real money.
Practice (0/3 done)
Write updateList(notes, action) that returns a NEW notes array after a server answer. action is one of: { type: "created", note } (put the note at the start), { type: "updated", note } (replace the note with the same id), { type: "deleted", id } (remove the note with that id). For any other type, return a copy of the list. Never change the input array.
Write async togglePinOptimistic(notes, id, saveFn, show). 1) Build a NEW list where the note with that id has pinned flipped (true to false or false to true); other notes stay the same. 2) Call show(newList) at once, BEFORE waiting for the server. 3) await saveFn(id, newPinnedValue). 4) If it works, return the new list. If saveFn throws, roll back: call show(notes) with the original list and return the original list. Do not change the input array or its notes.
Write createLatestSearch(searchFn, show). It returns an async function search(text). Each call to search awaits searchFn(text) (which returns a Promise of results) and then calls show(results), but ONLY if no newer search was started in the meantime. Answers to older searches that arrive late must be ignored. Tip: keep a counter outside search and give each call its own number.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.