State management with Zustand

Since we’re exploring Next.js lately, let’s see how we achieve client-side state management in React or Next.js. For basic level of state management such as defining a component level variable, we normally use React’s useState hook. For more advanced use cases, we have dedicated libraries such as Redux or Zustand. In this tutorial we’ll use Zustand within a Next.js app in order to incorporate client-side state management which is persisted app-wide.

Assuming you have Node.js installed in your system, enter following command in terminal to create a Next.js app:

npx create-next-app@latest

Keep pressing Enter to select the default options and you are good to go. Once the installation is finished, go inside the newly created folder my-app:

cd my-app

Now, install Zustand by entering following command:

npm install zustand

At this stage (or later), you may execute following command to run development server:

npm run dev

Open your browser and enter following URL to check everything is working fine:

localhost:3000

At this point, you may also choose to open my-app folder in your favorite code editor.

Now, let’s create the folder stores by executing following command inside my-app folder:

mkdir stores

We’ll be using this stores folder to keep Zustand related stuff. So, create a new file store.ts inside stores folder by executing:

touch stores/store.ts

and then insert following code into stores/store.ts:

import {create} from 'zustand'
import { persist } from 'zustand/middleware'

interface countState {
  counter: number;
  oneUp: () => void;
  reset: () => void;
}

const useCounter = create<countState>()(
  persist(
    (set, get) => ({
	  counter: 0,
      oneUp: () => set({ counter: get().counter + 1 }),
  	  reset: () => set({ counter:0 }),
    }),
    {
      name: 'my-store',
    }
  )
)

export default useCounter

Next, create another file useStore.ts inside stores folder:

touch stores/useStore.ts

and then insert following code inside stores/useStore.ts:

import { useState, useEffect } from 'react'

const useStore = <T, F>(
  store: (callback: (state: T) => unknown) => unknown,
  callback: (state: T) => F
) => {
  const result = store(callback) as F
  const [data, setData] = useState<F>()

  useEffect(() => {
    setData(result)
  }, [result])

  return data
}

export default useStore

All set…we’ve defined persistent data at the client-side using Zustand library. Let’s check it now – so, open app/page.tsx and replace all the code inside it with following:

'use client'

import useCounter from '@/stores/store'
import useStore from '@/stores/useStore'

export default function Home() {

const counter = useStore(useCounter, (state) => state.counter)
const oneUp= useCounter((state) => state.oneUp)
const reset= useCounter((state) => state.reset)

  return (
    <main className="flex min-h-screen flex-col items-center p-4">
      <p className="p-10 bg-zinc-600 text-white font-bold py-2 px-4 rounded">
        Welcome on Homepage
      </p>
      <div className="w-full max-w-5xl items-center justify-between font-mono text-sm">
        <div className="m-2">
    			There are {counter} items.
    		</div>
        <button onClick={oneUp} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded m-2">one up</button>
        <button onClick={reset} className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded m-2">Remove All</button>
      </div>
    </main>
  )
}

Now, head to browser and enter following URL assuming dev server is already running:

localhost:3000

Add some counts, close browser, reopen browser and enter the above URL again. To check whether the store’s data is persisting app-wide, let’s create another route inside our Next.js app by executing following command:

mkdir app/second

Now, create page.tsx inside app/second:

touch app/second/page.tsx

And then insert following code inside app/second/page.tsx:

'use client'

import useCounter from '@/stores/store'
import useStore from '@/stores/useStore'

export default function Second() {

const counter = useStore(useCounter, (state) => state.counter)
const oneUp= useCounter((state) => state.oneUp)
const reset= useCounter((state) => state.reset)

  return (
    <main className="flex min-h-screen flex-col items-center p-4">
      <p className="p-10 bg-lime-600 text-white font-bold py-2 px-4 rounded">
        This is Second Page
      </p>
      <div className="w-full max-w-5xl items-center justify-between font-mono text-sm">
        <div className="m-2">
    			There are {counter} items.
    		</div>
        <button onClick={oneUp} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded m-2">one up</button>
        <button onClick={reset} className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded m-2">Remove All</button>
      </div>
    </main>
  )
}

Save and then enter following URL in the browser:

localhost:3000/second

Observe that the store’s data is persisting app-wide.

Leave a Comment