zustand persist in nextjs14

353 Views Asked by At

I am using zustand to persist my number, but the number on the refresh page is blinking, what can I do to fix it.

enter image description here

I'm not sure what caused this problem, it seems to be a server and client hydration problem caused by incorrect

this is my app/page.tsx

'use client'
import Button from '@mui/material/Button'
import { useCountStore } from '@/app/store/useCountStore'
import useStore from './store/useStore'

const Home = () => {
  const countStore = useStore(useCountStore, (state) => state)
  return (
    <>
      <div className='text-[red] text-[24px]'>{countStore?.num}</div>
      <Button onClick={countStore?.increaseNum} variant='contained'>increase number</Button>
      <Button onClick={countStore?.decreaseNum} variant='contained'>decrease number</Button>
    </>
  )
}
export default Home

this is my store/useCountStore

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

interface countType {
    num: number,
    increaseNum: () => void,
    decreaseNum: () => void
}
export const useCountStore = create(persist<countType>((set, get) => ({
    num: 0,
    increaseNum: () => set({ num: get().num + 1 }),
    decreaseNum: () => set({ num: get().num - 1 }),
}), { name: 'count_storage_tag' }))

this is my store/useStore

'use client'
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
1

There are 1 best solutions below

0
teos On

I suspect that if you switch to countStore?.num ?? 0, it will display 0 instead of blinking (and that's better actually, but not the point)

This answer might be helpful (about cookies, but the underlying issue is the same)

In a nutshell:
This happens because the Client Component is still initally rendered (once) on the server. While the name can be a bit confusing, the idea is to provide the user with the best "empty HTML shell" possible before rehydrating in the user's browser.
That's great, but the server has no access to zustand state (it's client side only), and it ends up with a nullish value in countStore. The value appears once the client-side hydratation step is complete.

You could persist zustand state in cookies to share it with the server (which I do not recommend, except theme variables maybe). Most likely, you should keep the state client side, and have the server default to a skeleton version of your component.
As long as the skeleton and the actual content are the same height, the UX should be great.

For example:

<div className='text-[red] text-[24px]'>{countStore?.num ?? '-'}</div>

or,

{countStore
    ? <div className='text-[red] text-[24px]'>{countStore.num}</div>
    : <Skeleton />}