DEV Community

Andrzej S.
Andrzej S.

Posted on

How to reset 20 Zustand stores correctly on logout

A user logs out, another logs in on the same device - and suddenly sees the previous user's cart or permissions. Whether you have 3 stores or 20, resetting state on logout is a critical task that often falls through the cracks.

Why the "Typical" Reset Fails

The most common approach is usually: reset: () => set(initialState). However, this fails in four specific ways:

  1. Frozen dynamic values: If you have sessionId: crypto.randomUUID(), it was generated once at module load. Every "reset" restores that same ID. A real reset needs to re-run the initializer.
  2. Merge instead of replace: Zustand's set merges by default. Leftover keys from previous states survive. You need set(fresh, true) to perform a full replacement.
  3. Forgotten stores: A manual logout() function calling 20 different reset actions will eventually break the day someone adds store #21. You need a centralized registry.
  4. Persist race conditions: Resetting memory while localStorage is still being read (rehydration) can cause the old data to overwrite your fresh state. If storage fails, Zustand might swallow the error, hanging your logout logic forever.

The Solution: zustand-reset-manager

I hit all of these walls in my own projects, so I built a small utility to handle it: zustand-reset-manager. It’s MIT-licensed, has zero runtime dependencies, and supports Zustand v4/v5.

1. Define a Resettable Store

The initializer keeps its exact Zustand signature, including middleware.

import { createResettableStore } from 'zustand-reset-manager';

export const useCart = createResettableStore<CartState>("cart")((set) => ({
  items: [],
  sessionId: crypto.randomUUID(), // This will be re-generated on every reset!
  add: (item) => set((s) => ({ items: [...s.items, item] })),
}));


Enter fullscreen mode Exit fullscreen mode

Update (v0.4.0): the hooks promised in the comments are out, plus ordering - because with 20 stores the next question is always "but my session store must reset last".

One place to react to every reset - logging, analytics, cleanup:

const unsubscribe = addResetListener({
  beforeReset: ({ name, group, reason }) => console.log(`resetting ${name} (${reason})`),
  afterReset: ({ name }) => {},
});
Enter fullscreen mode Exit fullscreen mode

Declare order instead of hand-sequencing 20 calls:

export const useSession = createResettableStore(
  { name: "session", dependsOn: ["cart", "user"] },
  (set) => ({ /* ... */ }),
);

await resetAllStores({ reason: "logout" });
Enter fullscreen mode Exit fullscreen mode

dependsOn declares "reset me AFTER these" - bulk resets are topologically sorted (Kahn's algorithm), so cart and user reset before session, level by level; async resets are awaited per level, and independent stores within a level run in parallel. A cycle gets you a dev-mode warning and a safe fallback instead of a crash. reason is a free-form string that rides along to every hook, so "logout" vs "tenant-switch" vs "test" is one if away.

Top comments (3)

Collapse
 
topstar_ai profile image
Luis Cruz

I appreciate how you've highlighted the pitfalls of the typical reset approach, particularly the issue with frozen dynamic values and the need for a full replacement using set(fresh, true). The example you provided using zustand-reset-manager is really helpful in demonstrating how to define a resettable store, and I like how it supports the exact same Zustand signature, including middleware. Have you considered adding any additional features to zustand-reset-manager to handle edge cases, such as supporting multiple reset strategies or integrating with other state management libraries?

Collapse
 
phetphet profile image
Andrzej S.

Thanks! reset strategies - sort of, the next version will likely add hooks (beforeReset/afterReset) and reset ordering between stores. preserve already covers partial resets. Other libraries - no plans at the moment

Some comments may only be visible to logged-in visitors. Sign in to view all comments.