HuntExams Academy logo
ReactAdvanced#state#data

What is the difference between server state and client state?

Answer

Server state is data that lives on a server and is only cached in your React app: users, products, orders \u2014 anything fetched over the network. Client state (UI state) is data that is born and dies in the browser: whether a modal is open, the active tab, form input, theme, selected filters. Why the distinction matters: server state is shared, asynchronous, and can go stale without your app knowing. It needs loading and error states, caching, request deduplication, background refetching, retries, pagination and invalidation after a mutation. Client state needs none of that \u2014 it is synchronous, owned by you, and always up to date. Tooling follows the split: - Server state: TanStack Query (React Query), SWR, RTK Query, or a framework route loader. You get queryKey-based caching, staleTime and gcTime, refetch on window focus and reconnect, retry with backoff, and invalidateQueries after mutations. - Client state: useState or useReducer for local state, Context or Zustand/Jotai/Redux for shared UI state, and URL search params for anything that should survive a refresh and be shareable, such as filters, page number and active tab. The classic anti-pattern is treating server state as client state: fetching inside useEffect, copying the response into useState or a Redux store, then hand-writing loading flags, cache invalidation and refetch logic. It duplicates the server's truth, goes stale, and re-fetches the same data from five different components. Rule of thumb: if another user or another tab could change it, it is server state \u2014 cache it, do not own it. If only this browser session cares, it is client state.

Example

// SERVER STATE -- a cached copy of remote truth
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

const usersQuery = {
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
  staleTime: 60_000,
};

function Users() {
  const { data, isPending, isError } = useQuery(usersQuery);
  const qc = useQueryClient();

  const addUser = useMutation({
    mutationFn: (u) => fetch('/api/users', { method: 'POST', body: JSON.stringify(u) }),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['users'] }),  // refetch truth
  });

  if (isPending) return <Spinner />;
  if (isError)   return <Error />;
  return <List items={data} onAdd={addUser.mutate} />;
}

// CLIENT STATE -- pure UI concerns
const [isOpen, setIsOpen] = useState(false);    // modal
const [tab, setTab]       = useState('info');   // active tab
const [theme, setTheme]   = useState('dark');   // preference

// Shareable client state belongs in the URL
// /jobs?role=backend&page=2  -> survives refresh, can be copied

// ANTI-PATTERN: server state kept as client state
useEffect(() => {
  fetch('/api/users').then(r => r.json()).then(setUsers);  // no cache,
}, []);                                                    // no retry, goes stale

Detailed explanation

Server state is data whose authoritative source is a backend; React usually holds a fetched snapshot of it. Examples include users, products, orders, and account balances. Client state is owned by the frontend and represents interactions or presentation, such as an open modal, an active tab, or an unsaved form draft. The distinction is about ownership and lifecycle, not simply where a value is stored: putting fetched products into useState does not make them client-owned data.

Server state requires coordination with an external system. Requests take time, can fail, and may return data that another user changes immediately afterwards. A React application therefore needs policies for loading, errors, caching, freshness, concurrent requests, and updates after mutations. TanStack Query, SWR, and RTK Query provide server-state utilities; framework loaders can coordinate route-level fetching, with caching capabilities varying by framework. In TanStack Query, query keys identify cached results and must include relevant inputs, such as filters and page numbers. staleTime controls how long data is considered fresh, while gcTime controls when inactive cached data is removed; becoming stale does not itself trigger an immediate request.

Client state usually needs simpler update rules. Use useState for local values, useReducer for related transitions, and Context or a shared store when multiple components need access. Put shareable navigation state, such as filters or pagination, in URL search parameters. Client state can also persist in localStorage or synchronise across tabs, so it is not necessarily temporary or always current. React state updates are scheduled rather than immediately mutating the current render's values.

For example, a product list is server state, the selected category is client or URL state, and an edited product form is a local draft until saved. After a successful save, update or invalidate relevant cached queries; optimistic updates may require rollback on failure. Interviewers look for this separation and its consistency trade-offs. Fetching in useEffect is not inherently wrong, but manually rebuilding caching, race handling, and refetching across components is often unnecessary and error-prone.

Key points to remember

  • Classify state by its authoritative owner, not by the hook or store holding it.
  • Server state needs freshness, request lifecycle, caching, and mutation-consistency policies.
  • Include every input affecting fetched results in the query key.
  • Keep UI state local unless sharing, persistence, or navigation requirements justify another location.
  • Treat editable drafts separately from cached server records and reconcile them when saving.

Common mistakes candidates make

  • Assuming all data in Redux or useState is client state, or that Redux cannot manage server state through RTK Query.
  • Copying query results into another store without a clear purpose, creating competing copies that can diverge.
  • Claiming client state is always synchronous and temporary, or that staleTime automatically schedules a refetch.

Likely follow-up questions

What happens when you invalidate a query after a mutation?

In TanStack Query, matching queries are marked stale, overriding their configured staleTime. By default, active matching queries refetch in the background; inactive ones are not immediately refetched.

Should server data ever be copied into local form state?

Yes, when the form needs an independently editable draft rather than a live view of the cached record. Decide how background updates affect dirty fields so a refetch does not silently overwrite the user's edits.

Can client state affect server state fetching?

Yes: a selected filter or page number can determine which server data is requested. Include those values in the query key so different result sets have distinct cache entries.

Interview tip: Start with ownership, contrast the lifecycle requirements, and finish with one example separating fetched data, UI controls, and an editable draft.

Related Interview Questions

1
ReactIntermediate#state

Why should you not mutate state directly?

Open
2
ReactIntermediate#routing

How do you handle routing in React?

Open
3
ReactBeginner#props

What are default props and prop validation today?

Open