DEV Community

Cover image for Global State in Next.js App Router — Zustand Over Context for Most Use Cases
Aon infotech
Aon infotech

Posted on

Global State in Next.js App Router — Zustand Over Context for Most Use Cases

React Context works. It's built-in, requires no dependencies, and handles many state management needs fine. It also causes the specific problem that leads developers to look for alternatives: unnecessary re-renders when the context value changes.

For small amounts of global state — a theme preference, a user session — Context is fine. For anything with more frequent updates or more complex structure, Zustand is meaningfully better and the migration is straightforward.

Here's the practical comparison and how I set up state management in the generation tool at pixova.io/blog/free-ai-logo-generator.


The Context Re-Render Problem

Context triggers a re-render in every component that consumes it whenever the context value changes — even if the specific piece of state the component cares about didn't change.

const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [preferences, setPreferences] = useState({});
  const [notifications, setNotifications] = useState([]);

  // If notifications updates, ALL consumers re-render
  return (
    <UserContext.Provider value={{ user, preferences, notifications }}>
      {children}
    </UserContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

The fix — splitting into multiple contexts — works but gets unwieldy quickly.


Zustand — Selector-Based Subscriptions

npm install zustand
Enter fullscreen mode Exit fullscreen mode
// lib/stores/userStore.ts
import { create } from 'zustand';

interface UserState {
  user: User | null;
  preferences: UserPreferences;
  notifications: Notification[];
  setUser: (user: User | null) => void;
  updatePreferences: (prefs: Partial<UserPreferences>) => void;
  addNotification: (notification: Notification) => void;
  clearNotifications: () => void;
}

export const useUserStore = create<UserState>((set) => ({
  user: null,
  preferences: { theme: 'light', language: 'en' },
  notifications: [],

  setUser: (user) => set({ user }),
  updatePreferences: (prefs) =>
    set((state) => ({ preferences: { ...state.preferences, ...prefs } })),
  addNotification: (notification) =>
    set((state) => ({ notifications: [...state.notifications, notification] })),
  clearNotifications: () => set({ notifications: [] }),
}));
Enter fullscreen mode Exit fullscreen mode

Components subscribe only to what they need:

// Only re-renders when user changes — not when notifications change
function Header() {
  const user = useUserStore((state) => state.user);
  return <header>{user ? <span>{user.name