I remember the exact moment I finally admitted to myself that my frontend state management was a mess. I was debugging a dashboard that showed user activity logs, and the data on screen was eight minutes stale. Meanwhile, a sidebar toggle kept resetting every time the component re-rendered. I had stuffed everything—server responses, UI flags, form inputs—into a single, bloated Redux store. The code was fragile, the state was chaotic, and I was spending more time tracking down bugs than building features.
That experience pushed me to look for a cleaner separation. I knew server state and client state are fundamentally different. Server state is asynchronous, cacheable, and lives outside your app. Client state is synchronous, ephemeral, and belongs entirely to the user’s current session. Treating them the same way is like storing milk and cereal in the same container. You can do it, but the result is never pleasant.
That is why I turned to Zustand for client state and React Query for server state. Individually, each library is excellent at its job. Together, they provide a clear, scalable, and surprisingly lightweight foundation for any React application. No more mixing concerns. No more stale data creeping into your UI. Just two focused tools that know their role and do it well.
Have you ever stored an API response in a global store and then had to manually refetch it every few seconds to keep it fresh? That is the kind of work React Query eliminates. It handles caching, background refetching, retries, and pagination out of the box. You write a single query hook, and the library takes care of the rest. Zustand, on the other hand, remains completely out of your way for anything that is not server data. You create a small store for your theme, your modal visibility, your sidebar width, and you never have to think about network requests inside that store.
Let me show you how this looks in practice. I will start with a Zustand store for a simple UI preference: whether a sidebar is open.
import { create } from 'zustand';
const useSidebarStore = create((set) => ({
isOpen: true,
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}));
That is it. No actions, no reducers, no ceremony. Two lines of store logic and you have a globally accessible piece of state that any component can read or update. Now the server side. I will use React Query to fetch a list of tasks from an API.
import { useQuery } from '@tanstack/react-query';
const fetchTasks = async () => {
const response = await fetch('/api/tasks');
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
};
export function useTasks() {
return useQuery({
queryKey: ['tasks'],
queryFn: fetchTasks,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
Notice I set a staleTime. React Query will not refetch the data unless five minutes have passed or the user triggers a manual refetch. This reduces unnecessary network requests and keeps the UI responsive. Now, in a component that needs both pieces of state, the separation is crystal clear.
import { useTasks } from './hooks/useTasks';
import { useSidebarStore } from './stores/sidebarStore';
function Dashboard() {
const { data: tasks, isLoading, error } = useTasks();
const { isOpen, toggle } = useSidebarStore();
if (isLoading) return <div>Loading tasks...</div>;
if (error) return <div>Error loading tasks: {error.message}</div>;
return (
<div className="dashboard">
<button onClick={toggle}>
{isOpen ? 'Close' : 'Open'} Sidebar
</button>
{isOpen && <Sidebar />}
<TaskList tasks={tasks} />
</div>
);
}
There is no confusion. The tasks come from the server, and the sidebar visibility is a local UI decision. Want to update the tasks? Use React Query’s useMutation. Want to persist the sidebar state across sessions? Add a Zustand middleware like persist that saves to localStorage. The libraries never overlap, and you never have to write complex synchronization logic.
I have seen teams try to implement this pattern with just Redux or just Context API, and it always ends in pain. Either they cache server data manually with useEffect and setState, or they pollute their store with stale copies of API responses. With Zustand and React Query, you get a clean contract. The server owns the truth, and the client owns the experience.
A common question I hear is: “Do I really need a library for client state? Can’t I just use React context or local state?” You can, but as your application grows, you will find yourself prop drilling or wrapping everything in providers. Zustand gives you a global store that works outside of React’s component tree, which is useful for accessing state in event handlers or utility functions. React Query, meanwhile, is almost mandatory for any serious data fetching because it eliminates boilerplate and handles edge cases like race conditions and request deduplication automatically.
What about performance? Both libraries are built with efficiency in mind. Zustand does not cause unnecessary re-renders because components only subscribe to the specific slice of state they use. React Query does the same by tracking query keys and only updating components when their data actually changes. Together, they form a system that scales without bloat.
In my own projects, I have used this combination for everything from a small blog platform to a real-time collaboration tool. The setup is always the same: a few lines of Zustand for UI flags, and a handful of custom hooks for server interactions. The result is code that is easy to read, easy to test, and easy to refactor. I no longer dread opening a state management file because I know exactly where each piece belongs.
If you are tired of wrestling with state management, I encourage you to give this pairing a try. Start by moving one server fetch out of your global store and into React Query. Then create a small Zustand store for one piece of UI state. Feel how much lighter your mental load becomes. You will wonder why you ever did it any other way.
Now I would like to hear from you. Have you tried separating server and client state in your projects? What approach worked best for your team? Drop a comment below. If this article helped you see state management in a new light, please like and share it with someone who might need the same clarity. Your feedback drives me to dig deeper and write better content. Let’s keep building smarter, cleaner applications together.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva