Unlocking Progressive State Management with Zustand – Is There a Silver Bullet?
This article walks through using the Zustand state‑management library in a complex React component, covering store initialization, actions, selectors, modular file structure, handling controlled vs uncontrolled modes, performance optimizations, and devtools integration, while sharing practical tips and code examples for scalable frontend development.
Is There a Silver Bullet?
In the previous article the author introduced the motivations for using Zustand in the ProEditor scenario. This piece details how to solve all state‑management requirements with Zustand from a practical perspective.
There is no silver bullet. Choose a remote‑state library if it meets your needs; otherwise use useState + Context for simple cases, or consider external stores like Valtio or Zustand for more complex scenarios.
The core challenge is whether the current approach can handle more complex situations, such as expanding from 5 to 15 states, integrating a canvas chart, or extracting a component for external reuse.
Developer experience (DX) of the library.
Ability to accommodate future complex scenarios with low cost.
Progressive State Management with Zustand
The author uses an icon selector component as a real‑world example. The component must display, select, delete, search icons, switch between Ant Design and Iconfont sources, and add or remove Iconfont scripts.
Step 1: Store Initialization – State
Create
store.tsand define the initial state:
<code>import create from 'zustand';
// useStore is the hook
export const useStore = create(() => ({
panelTabKey: 'antd',
// other state fields …
}));</code>In React components import
useStoreto read
panelTabKeyand use
useStore.setStateto modify it.
Step 2: Actions – State‑Changing Methods
Define custom actions such as
selectIconthat update multiple pieces of state and can be reused across components.
<code>export const useStore = create(set => ({
// …
selectIcon: icon => {
set({ icon, open: false, filterKeywords: undefined });
},
}));</code>Actions support async/await and are memoized by Zustand, avoiding unnecessary re‑renders.
Step 3: Selector – Derived State
Complex derived state like
iconListis implemented with selectors that filter based on the active tab and search keywords.
<code>export const displayListSelector = s => {
const list = s.panelTabKey === 'iconfont' ? s.iconfontIconList : s.antdIconList;
const { filterKeywords } = s;
return list.filter(i => {
if (!filterKeywords) return true;
switch (i.type) {
case 'antd':
case 'internal':
return i.componentName.toLowerCase().includes(filterKeywords.toLowerCase());
case 'iconfont':
return i.props.type.toLowerCase().includes(filterKeywords);
}
});
};</code>Step 4: Organizing Files and Types
Split the store into three files:
initialState.ts– State type and initial values.
createStore.ts– Store creation and actions.
selectors.ts– Selector functions.
Export them from
index.tsfor easy consumption.
Step 5: Complex Actions with get()
Use
get()inside actions to read the whole store, enabling multi‑step operations such as adding a script, hiding the form, updating the script list, and selecting the new script.
Step 6: From Application to Component – Context and StoreUpdater
Wrap the store with
createContextto create a provider, allowing multiple isolated instances. Implement a
StoreUpdatercomponent that syncs external props (e.g.,
icon,
iconfontScripts) to the internal store, supporting controlled usage.
Step 7: Performance Optimization – Selectors
Apply shallow‑compare selectors to subscribe only to the needed slices of state, preventing re‑renders when unrelated fields change.
<code>import shallow from 'zustand/shallow';
const selector = s => ({ panelTabKey: s.panelTabKey, icon: s.icon, resetIcon: s.resetIcon });
const { panelTabKey, icon, resetIcon } = useStore(selector, shallow);
</code>Step 8: DevTools Integration
Wrap the store with
devtoolsmiddleware and provide descriptive action names for better debugging.
<code>export const createStore = () =>
create(devtools((set, get) => ({
// state and actions …
}), { name: 'IconPicker' }));
</code>Optionally pass a third argument to
setfor a custom label, e.g.,
set({ icon }, false, 'Select Icon').
Additional notes mention possible extensions such as integrating Redux reducers, RxJS, SWR, persistence, history (zundo), subscriptions, slice‑based stores, and third‑party libraries like Y‑js.
Alipay Experience Technology
Exploring ultimate user experience and best engineering practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.