One of the downsides to Controller/Reducer-based state management is that a single piece of state needs to be passed from Component to Component up and down the cascade - regardless of whether that component needs the state or not.
This can cause an application to run slower than is desirable.
Zustand promises to alleviate this issue. And, to cut a long story short, it does.
Observe the Hideous Spaceship of StateControllers!
ReactDOM.render(
<React.StrictMode>
<AppState>
<UserState>
<DownloadState>
<DocumentsState>
<ShareState>
<ModalState>
<ViewerState>
<InteractionsState>
<NotificationsState>
<App />
</NotificationsState>
</InteractionsState>
</ViewerState>
</ModalState>
</ShareState>
</DocumentsState>
</DownloadState>
</UserState>
</AppState>
</React.StrictMode>,
document.getElementById("root")
);
This means that a piece of information from say ModalState
has to be passed to ViewerState
to InteractionsState
to NotificationsState
before the final destination of <App/>
. This causes several unnecessary re-renders as well as being slow, costly, and frankly irritating.
Zustand uses a “Hook-based” approach that doesn’t pass state around components that do not directly need it.
This approach greatly reduces the “spaceship” and looks a little like this;
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</React.StrictMode>
)
Creating a Store
// Store/app.jsx
import create from 'zustand'
const useAppStore = create((set, get) => ({
openSidebar: () => set(state => ({ sidebarIsOpen: true })),
closeSidebar: () => set(state => ({ sidebarIsOpen: false })),
sidebarIsOpen: false,
});
export default useAppStore;
Now we have a store we can import
wherever we need to.
// Components/Nav.jsx
import useAppStore from '../../Store/app'
const Nav = (
) => {
const openSidebar = useAppStore(state => state.openSidebar)
return (
<nav className="primary-nav">
<button onClick={openSidebar}>🍔</button>
<ul className="nav-list">...</ul>
</nav>
)
}
export default Nav
// Components/Sidebar.jsx
import useAppStore from '../../Store/app'
const Sidebar = ({ children }) => {
const isSidebarOpen = useAppStore(state => state.isSidebarOpen)
const closeSidebar = useAppStore(state => state.closeSidebar)
return (
<aside open={isSidebarOpen} className="sidebar">
<button onClick={closeSidebar}>❎</button>
{children}
</aside>
)
}
export default Sidebar
This way, the only Components that are affected by the change in isSidebarOpen
are the Nav
and the Sidebar
. The main body of the app, for example, doesn’t have any knowledge of the state of the sidebar (because it doesn’t need to know).
// Components/Main.jsx
const Main = ({ children }) => <main>{children}</main>
export default Main
However, if we need to, we can make the Main
aware of the state change as easily as import
ing the Store…
// Components/Main.jsx
import useAppStore from '../../Store/app'
const Main = ({ children }) => {
const sidebarIsOpen = useAppStore(state => state.sidebarIsOpen)
return <main className={sidebarIsOpen && 'blur'}>{children}</main>
}
export default Main
Amending state
Imagine, a while down the line, we get the request to track “sidebar opens” (for whatever mad reason - clients, eh?!). The only file we need to change is the Store.
// Store/app.jsx
import create from 'zustand'
import Analytics from 'analytics'
const useAppStore = create((set, get) => ({
openSidebar: () => {
Analytics.log({ event: 'sidebarOpen', timestamp: new Date() })
set(state => ({ sidebarIsOpen: true }))
},
closeSidebar: () => set(state => ({ sidebarIsOpen: false })),
sidebarIsOpen: false,
});
export default useAppStore;
Conclusion
The learning curve is shallow enough - even for someone like me who isn’t the most React-savvy! It makes the state much more readable and replaceable too.
Top comments (0)