State & Data Flow: React Query, Context & Optimistic Updates
Overview
Once you start fetching data, managing its state across your application becomes critical. This tutorial covers lifting state up, distinguishing server state from client state, using React Query (TanStack Query) for powerful data synchronisation, implementing optimistic updates for a snappy UI, and managing global state with Context or Zustand.
1. Lifting State Up
When multiple components need to share the same data, the state should be moved (lifted) to their closest common ancestor. This ancestor then passes the data and an update function down via props.
This works well for small to medium apps, but becomes cumbersome with deep nesting or many components – hence the need for Context or state management libraries.
2. Server vs Client State
It's crucial to distinguish between these two types of state:
- Client state: UI state (modals open/closed, form inputs, dark mode). Stored in React state or local storage.
- Server state: Data persisted on the server (posts, users, comments). Requires asynchronous fetching, caching, re‑validation, and synchronization.
Managing server state with useState + useEffect is
error‑prone (race conditions, caching, re‑fetching). Libraries like
React Query handle this for you.
3. React Query (TanStack Query)
React Query is a powerful library that handles server‑state management: caching, background updates, retries, pagination, and more.
Setup
useQuery – fetching data
useMutation – modifying data
queryKey uniquely identifies cached data.
invalidateQueries triggers a refetch.
React Query handles caching, deduping, and background refetching automatically.
4. Optimistic Updates
Optimistic updates update the UI immediately, before the server confirms the change. If the server request fails, we roll back to the previous state. This makes the UI feel instant and responsive.
This pattern is used in many applications (e.g., liking a post, adding a comment) to provide a seamless user experience.
5. Global State (Context / Zustand)
For client‑side state that is needed across many components (e.g., user theme, authentication status), you can use the Context API or a dedicated state management library like Zustand or Redux.
Context API (built‑in)
Zustand (lightweight alternative)
Recommendation: Use React Query for server state. Use Context or Zustand for client state (theme, auth, modals). This separation keeps your application clean and performant.
Quiz
Question 1
What is the primary purpose of React Query?
- To manage UI state
- To handle server‑state (caching, fetching, synchronisation)
- To replace Redux for all state
- To handle routing
Show answer
Question 2
Which React Query method is used to trigger a refetch of queries after a mutation?
- refetchQueries
- invalidateQueries
- resetQueries
- clearQueries
Show answer
invalidateQueries.Question 3
What does an optimistic update do?
- It waits for the server response before updating the UI
- It updates the UI immediately and rolls back if the server fails
- It updates the server without changing the UI
- It disables the UI during the request
Show answer
Exercises
Exercise 1
Write a React Query useQuery hook to fetch a list of posts from
/api/posts with a query key of ['posts']. Show loading and error
states.
Sample answer
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => axios.get('/api/posts').then(res => res.data),
});
if (isLoading) return <div>Loading posts...</div>;
if (error) return <div>Error loading posts: {error.message}</div>;
return <ul>{data.map(post => <li key={post.id}>{post.title}</li>)}</ul>;
Exercise 2
Explain the role of onMutate in a React Query mutation for
optimistic updates.
Sample answer
onMutate runs before the actual mutation function. It allows
you to:
- Cancel any outgoing refetches to avoid overwriting the optimistic update.
- Snapshot the current cache state.
- Optimistically update the cache with the new data.
- Return a context object (e.g., the snapshot) to be used in
onErrorfor rollback.
Homework
Homework 1
Create a TodoList component that uses React Query to fetch and
display todos. Implement a mutation to add a new todo with optimistic updates. Ensure that
if the add request fails, the UI rolls back to the previous state.
Sample answer (skeleton)
Fetch:
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
Mutation:
const mutation = useMutation({
mutationFn: addTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries(['todos']);
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { previous };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries(['todos']);
},
});
Mini‑Project
Collaborative Task Board (Kanban)
Build a simple task board with three columns: "To Do", "In Progress", "Done".
- Fetch tasks from a mock API using React Query.
- Allow dragging a task to a different column (or clicking a button to move it).
- Implement optimistic updates for moving tasks.
- Use Context or Zustand for UI state (e.g., which column is expanded).
Sample architecture
Data structure:
// Each task: { id, title, status: 'todo' | 'inProgress' | 'done' }
React Query:
useQuery(['tasks'], fetchTasks)useMutation(updateTaskStatus)with optimistic update that modifies the task's status in the cache.
Optimistic update:
onMutate: async ({ taskId, newStatus }) => {
await queryClient.cancelQueries(['tasks']);
const previous = queryClient.getQueryData(['tasks']);
queryClient.setQueryData(['tasks'], (old) =>
old.map(task => task.id === taskId ? { ...task, status: newStatus } : task)
);
return { previous };
}
UI state: Use Zustand to store which columns are collapsed/expanded.
Tutorial Summary
This tutorial covered the essential patterns for managing data flow in React applications. You learned about lifting state, distinguishing server from client state, using React Query for robust data synchronisation, implementing optimistic updates for a responsive UI, and managing global state with Context or Zustand. The mini‑project brought these patterns together in a real‑world scenario.
Key takeaway: Separate your concerns: React Query for server state, React state or Context/Zustand for client state. Leverage optimistic updates to make your app feel instantaneous while maintaining data integrity.