pmndrs / zustand

🐻 Bear necessities for state management in React

Home Page:https://zustand-demo.pmnd.rs/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Can't use persist with context implementation mentioned in the docs for Next.js

1Mouse opened this issue · comments

Discussed in #2350

Originally posted by 1Mouse February 21, 2024
my code is the same as docs https://docs.pmnd.rs/zustand/guides/nextjs#app-router
I want to prevent hydration errors when using persist middleware

store file

import { devtools, persist } from "zustand/middleware";
import { createStore } from "zustand/vanilla";

export type MainStoreState = {
  selectedPropertyId: number | undefined;
  selectedBusinessOwnerId: number | undefined;
  selectedBusinessOwnerName: string | undefined;
};

export type MainStoreActions = {
  updateSelectedPropertyId: (id: number) => void;
  updateSelectedBusinessOwner: (id: number, name: string) => void;
};

export type MainStore = MainStoreState & MainStoreActions;

export const initMainStore = (): MainStoreState => {
  return {
    selectedPropertyId: undefined,
    selectedBusinessOwnerId: undefined,
    selectedBusinessOwnerName: undefined,
  };
};

export const defaultInitState: MainStoreState = {
  selectedPropertyId: undefined,
  selectedBusinessOwnerId: undefined,
  selectedBusinessOwnerName: undefined,
};

export const createMainStore = (
  initState: MainStoreState = defaultInitState
) => {
  return createStore<MainStore>()(
    devtools(
      persist(
        set =>
          ({
            ...initState,
            updateSelectedPropertyId: (id: number) => {
              set(state => ({ ...state, selectedPropertyId: id }));
            },
            updateSelectedBusinessOwner: (id: number, name: string) => {
              set(state => ({
                ...state,
                selectedBusinessOwnerId: id,
                selectedBusinessOwnerName: name,
              }));
            },
          }) satisfies MainStore,
        {
          name: "main-store", // name of the item in the storage (must be unique)
          skipHydration: true, // do not rehydrate this store after a full page load, we will rehydrate it manually
        }
      )
    )
  );
};

provider file

"use client";

import { type ReactNode, createContext, useContext, useRef } from "react";

import { type StoreApi, useStore } from "zustand";

import {
  type MainStore,
  createMainStore,
  initMainStore,
} from "@/stores/mainStore";

export const MainStoreContext = createContext<StoreApi<MainStore> | null>(null);

export interface MainStoreProviderProps {
  children: ReactNode;
}

export const MainStoreProvider = ({ children }: MainStoreProviderProps) => {
  const storeRef = useRef<StoreApi<MainStore>>();
  if (!storeRef.current) {
    storeRef.current = createMainStore(initMainStore());
  }

  return (
    <MainStoreContext.Provider value={storeRef.current}>
      {children}
    </MainStoreContext.Provider>
  );
};

export const useMainStore = <T,>(selector: (store: MainStore) => T): T => {
  const mainStoreContext = useContext(MainStoreContext);

  if (!mainStoreContext) {
    throw new Error(`useMainStore must be use within MainStoreProvider`);
  }

  return useStore(mainStoreContext, selector);
};

the docs say that we can use this hook for nextjs

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

but that hook doesn't work with array destructuring syntax

const [
    selectedPropertyId,
    updateSelectedPropertyId,
    selectedBusinessOwnerId,
    selectedBusinessOwnerName,
    updateSelectedBusinessOwner,
  ] = useMainStore(state => [
    state.selectedPropertyId,
    state.updateSelectedPropertyId,
    state.selectedBusinessOwnerId,
    state.selectedBusinessOwnerName,
    state.updateSelectedBusinessOwner,
  ]);

so I went to try the other solution with skipHydration: true

and manually hydrate at the subscribed component inside useEffect

useEffect(() => {
    useMainStore.persist.rehydrate();
  }, []);

but I get this error Property 'persist' does not exist on type '(selector: (store: MainStore) => T) => T'.ts(2339)

Keep discussing in #2350