I’ve been building React applications for years, and a persistent challenge keeps resurfacing. How do you keep your application’s state in sync with where the user is navigating? It’s like trying to coordinate two separate conversations happening in the same room. You have your UI state managed neatly in one corner, and your routing logic directing traffic in another. They need to talk to each other, but the connection often feels clunky. This friction is what led me to explore combining two of my favorite tools: Zustand for state and React Router for navigation.
Zustand offers a breath of fresh air in state management. It cuts through the complexity, giving you a straightforward way to create a store with minimal code. React Router, on the other hand, is the trusted guide for moving users through a single-page application. They are both excellent at their individual jobs. The magic happens when you connect them, creating a unified system where state understands navigation and navigation can be guided by state.
Think about a common scenario: a multi-step checkout process. Each step is a different route—/cart, /shipping, /payment. As a user fills out their shipping address, that data needs to survive the jump to the payment page. You could lift all that state up to a common parent component, but that can get messy fast. With a Zustand store, you can keep that form data centralized and accessible from any component, regardless of its place in the route hierarchy.
Here’s a basic setup. First, you create a store that holds your navigation-sensitive state.
import { create } from 'zustand';
const useCheckoutStore = create((set) => ({
shippingInfo: null,
paymentMethod: null,
setShippingInfo: (info) => set({ shippingInfo: info }),
clearForm: () => set({ shippingInfo: null, paymentMethod: null }),
}));
Now, any component in your app can read or update the shipping info. But what if you want to clear this store when the user navigates away from the checkout flow entirely, say back to the homepage? This is where React Router comes in. You can use its useLocation hook inside a component to listen for route changes.
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import useCheckoutStore from './stores/checkoutStore';
const NavigationListener = () => {
const location = useLocation();
const clearForm = useCheckoutStore((state) => state.clearForm);
useEffect(() => {
// If the user navigates outside the checkout paths, clear the form data
if (!location.pathname.startsWith('/checkout')) {
clearForm();
}
}, [location.pathname, clearForm]);
return null; // This component doesn't render anything
};
You would render this NavigationListener high up in your app, perhaps in the root component. Now, your state management is aware of routing events. But can we go the other way? Can our state influence routing decisions? Absolutely. Consider an authentication flow. Your Zustand store might hold the user’s login status.
const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
login: (userData) => set({ user: userData, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}));
How do you protect a /dashboard route so only authenticated users can see it? You can create a wrapper component that checks the Zustand state and redirects the user if needed.
import { Navigate, useLocation } from 'react-router-dom';
import useAuthStore from './stores/authStore';
const ProtectedRoute = ({ children }) => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const location = useLocation();
if (!isAuthenticated) {
// Redirect to login, but save the location they tried to visit
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
};
Then, in your route configuration, you simply wrap the protected component.
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
This pattern is incredibly powerful. It moves authorization logic out of your routing definitions and into your state management, which is often more intuitive. The state becomes the single source of truth for whether a navigation action should succeed. Have you ever filled out a long form, accidentally clicked a link, and lost all your work? This integration can help prevent that. You can set a hasUnsavedChanges flag in your Zustand store and prompt the user before they navigate away, using React Router’s useBlocker hook.
The combination shines for preserving UI state, too. Imagine a dashboard with multiple data tables, each on its own tab under routes like /analytics/overview and /analytics/users. Each table might have its own sort order, filter settings, and pagination page. Storing this UI state in a Zustand store keyed by the route path means those settings persist when the user switches tabs and returns. The user experience becomes seamless and predictable.
Some might ask, “Why not just put everything in the URL?” URL parameters are perfect for state you want to be shareable, like a search query or a selected item ID. But they are not ideal for transient UI state, complex form objects, or sensitive data you wouldn’t want in a browser history. Using Zustand gives you a clean separation. Shareable, navigation-critical state goes in the URL; everything else lives in the reactive, component-accessible store.
This approach keeps your code simple. There’s no need for heavy context providers wrapping your entire app. Zustand stores are created independently and consumed on demand. React Router handles the navigation mechanics. You just build the bridges between them where it makes sense for your application. The result is an app that feels cohesive, where state and location work together instead of existing in isolation.
Getting these two tools to work in concert has fundamentally changed how I structure applications. It turns navigation from a simple page switcher into an integral part of the application’s state logic. The patterns are flexible, the code is minimal, and the user experience improves dramatically. It solves that old problem of disconnected conversations, letting state and routing finally speak the same language.
If you’ve struggled with keeping your app’s state in sync with your user’s journey, I highly recommend giving this pattern a try. It might just simplify your next project. Did you find this approach helpful? Have you tried similar integrations? I’d love to hear about your experiences—share your thoughts in the comments below!
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