Skip to content
TaeyoungKim.dev

Zustand store or React local state? Cart count versus panel visibility

WebWritten 3 min readTaeyoungKim
LinkedInX

The cart count appears in both the header and product view. Whether the cart panel is open, however, matters only to the panel and its button. Putting both values in one shared store simply because both are state obscures their ownership. Ask who reads and changes each value before choosing between Zustand and local React state.

Consider shared values for a store

The diagram places open in CartPanel and count in the shared store. Their owners differ because the number of consumers differs.

Assume several components need the cart count. Create a Zustand store with create, and select only the values or actions a component needs.

jsx
import { create } from 'zustand';
import { useState } from 'react';

const useCartStore = create((set) => ({
  count: 0,
  addOne: () => set((state) => ({ count: state.count + 1 })),
}));

function HeaderCount() {
  const count = useCartStore((state) => state.count);
  return <span>Cart {count}</span>;
}

function CartPanel() {
  const [open, setOpen] = useState(false);
  const count = useCartStore((state) => state.count);
  const addOne = useCartStore((state) => state.addOne);

  return (
    <section>
      <button type="button" onClick={() => setOpen((value) => !value)}>
        {open ? 'Close cart' : 'Open cart'}
      </button>
      {open && <button type="button" onClick={addOne}>Add one: {count}</button>}
    </section>
  );
}

When count changes, the header and panel read the same count. open controls only the panel's visibility, so useState keeps it close to that component. Putting it in the shared store would extend the scope of a change other screens do not need. This small example illustrates state ownership; it is not checkout logic.

Why use a selector?

useCartStore((state) => state.count) selects count rather than the entire store. The Zustand introduction shows this basic store and selector pattern. A component reading the entire store is more likely to be affected when unrelated fields change. Actual rerender behavior still depends on selected value identity and component structure; a selector is not a universal performance fix.

If a selector returns a newly created object containing several values, check whether its reference changes on each selection. Measure which components rerender frequently before splitting every field for performance reasons.

How can you test whether state lives in the right place?

Increment the count from a product view and verify the header also updates. Closing and reopening the panel should not reset the shared count. If opening a panel in one part of the app unexpectedly opens another, the open state may be shared too broadly.

If the count must survive a page reload, define persistence as a separate requirement; an in-memory shared store alone does not provide it. If an account's cart is owned by a server, the client store is not the final source of truth. Design around server responses, authorization boundaries, concurrent changes, and refresh behavior.

Key takeaways: decide by scope and lifetime

A count read by multiple components is a candidate for a Zustand store. Whether one panel is open naturally belongs in local useState. Select only the values needed, and treat server ownership and reload persistence as separate decisions.

Author

TaeyoungKim

Connecting technical foundations with implementation, verification, and production decisions.

#React#Zustand#useState#Shared state#Selector

Read next