DEV Community

Thomas Rigby
Thomas Rigby

Posted on • Originally published at thomasrigby.com on

Using Zustand to manage application state in React

A bike, for no reason

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")
);

Enter fullscreen mode Exit fullscreen mode

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>
)

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

However, if we need to, we can make the Main aware of the state change as easily as importing 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

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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)