Keeping UI state and server data in sync—cache, invalidation, and when to refetch.
Keeping UI state and server data in sync—cache, invalidation, and when to refetch.
Lesson outline
Client state lives only in the browser: form inputs, UI toggles, which tab is active. It does not need to match the server until the user submits or navigates. Server state is data that came from the backend (user profile, list of items, settings). The client has a *copy* that might become stale when the server changes (e.g. another tab or device updates it).
Sync means: when should the client refetch or revalidate server state? And when the user mutates (create, update, delete), how do you update the UI and optionally invalidate related data so the next read is fresh?

Simple pattern: when a component mounts, fetch the data (useEffect + fetch, or a data library). When the user performs a mutation (e.g. create order), send the request; on success, either refetch the list (GET again) or optimistically update the local state (add the new item to the list so the UI updates immediately; if the request fails, revert). Refetching is easy and correct; optimistic update is better UX but you must handle errors and rollback.
Many data libraries (React Query, SWR, Apollo) give you cache and invalidation: they store server data in a cache, and you can invalidate a key (e.g. "orders") after a mutation so the next read refetches. You get consistent "refetch after mutate" without writing it by hand.
Stale-while-revalidate: Show cached (possibly stale) data immediately, then fetch in the background and update the UI when the new data arrives. The user sees something fast; if the cache was stale, it gets replaced. This is a good default for read-heavy UIs (lists, dashboards). Libraries like SWR and React Query do this by default: they return cached data first, trigger a refetch, and re-render when the response arrives.
You can tune stale time (how long cache is considered fresh) and cache time (how long to keep unused data). Short stale time means more requests but fresher data; long stale time means fewer requests but possibly outdated data.
When the server needs to *push* updates (chat, notifications, live dashboard), HTTP request–response is not enough. You use WebSockets or Server-Sent Events (SSE) so the server can send messages to the client. The client still has local state (e.g. list of messages); when a new message arrives over the socket, you append it to the state and the UI updates. So "sync" here means: server pushes → client updates state → UI re-renders. This is in addition to (not a replacement for) your normal fetch-based reads and writes.
When the same data can change on the client and the server (e.g. edit in two tabs, or offline edit then reconnect), you need a conflict strategy: last-write-wins, merge fields, or operational transforms. Many apps avoid this by making mutations go through the server and refetching so the server is the single source of truth. If you need offline support, you cache server state locally and queue mutations when offline; when back online, replay and resolve conflicts. Libraries and backends (e.g. CRDTs, offline-first DBs) can help but add complexity.
Lists and dashboards: Stale-while-revalidate works well—show cache, refetch in background. Forms and settings: Fetch once on mount; on submit, send mutation and then refetch or invalidate so the next view is fresh. Real-time feeds: Use WebSockets or SSE and append to local state. Critical data (e.g. balance): Prefer shorter stale time or refetch on focus so the user sees up-to-date info. There is no single "right" strategy; match the UX and consistency requirements of each screen.
Ready to see how this works in the cloud?
Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.
View role-based pathsSign in to track your progress and mark lessons complete.
Questions? Discuss in the community or start a thread below.
Join DiscordSign in to start or join a thread.