diff --git a/readme.md b/readme.md index 1a1b3fd301..fd94573108 100644 --- a/readme.md +++ b/readme.md @@ -228,7 +228,7 @@ Zustand core can be imported and used without the React dependency. The only dif import { createStore } from 'zustand/vanilla' const store = createStore((set) => ...) -const { getState, setState, subscribe } = store +const { getState, setState, subscribe, getInitialState } = store export default store ``` diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index 5d8ad27ff4..e6eb5e1305 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -425,6 +425,8 @@ const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { api, ) + api.getInitialState = () => configResult + // a workaround to solve the issue of not storing rehydrated state in sync storage // the set(state) value would be later overridden with initial state by create() // to avoid this, we merge the state from localStorage into the initial state. diff --git a/src/react.ts b/src/react.ts index 48a35e3f89..0d73af98f3 100644 --- a/src/react.ts +++ b/src/react.ts @@ -28,6 +28,8 @@ type WithReact> = S & { let didWarnAboutEqualityFn = false +const identity = (arg: T): T => arg + export function useStore>>( api: S, ): ExtractState @@ -49,7 +51,7 @@ export function useStore>, U>( export function useStore( api: WithReact>, - selector: (state: TState) => StateSlice = api.getState as any, + selector: (state: TState) => StateSlice = identity as any, equalityFn?: (a: StateSlice, b: StateSlice) => boolean, ) { if ( @@ -65,7 +67,7 @@ export function useStore( const slice = useSyncExternalStoreWithSelector( api.subscribe, api.getState, - api.getServerState || api.getState, + api.getServerState || api.getInitialState, selector, equalityFn, ) diff --git a/src/traditional.ts b/src/traditional.ts index 478f986725..06bff299ec 100644 --- a/src/traditional.ts +++ b/src/traditional.ts @@ -26,6 +26,8 @@ type WithReact> = S & { getServerState?: () => ExtractState } +const identity = (arg: T): T => arg + export function useStoreWithEqualityFn>>( api: S, ): ExtractState @@ -41,13 +43,13 @@ export function useStoreWithEqualityFn< export function useStoreWithEqualityFn( api: WithReact>, - selector: (state: TState) => StateSlice = api.getState as any, + selector: (state: TState) => StateSlice = identity as any, equalityFn?: (a: StateSlice, b: StateSlice) => boolean, ) { const slice = useSyncExternalStoreWithSelector( api.subscribe, api.getState, - api.getServerState || api.getState, + api.getServerState || api.getInitialState, selector, equalityFn, ) diff --git a/src/vanilla.ts b/src/vanilla.ts index d5d9c092f0..df817932fe 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -8,6 +8,7 @@ type SetStateInternal = { export interface StoreApi { setState: SetStateInternal getState: () => T + getInitialState: () => T subscribe: (listener: (state: T, prevState: T) => void) => () => void /** * @deprecated Use `unsubscribe` returned by `subscribe` @@ -82,6 +83,9 @@ const createStoreImpl: CreateStoreImpl = (createState) => { const getState: StoreApi['getState'] = () => state + const getInitialState: StoreApi['getInitialState'] = () => + initialState + const subscribe: StoreApi['subscribe'] = (listener) => { listeners.add(listener) // Unsubscribe @@ -97,8 +101,8 @@ const createStoreImpl: CreateStoreImpl = (createState) => { listeners.clear() } - const api = { setState, getState, subscribe, destroy } - state = createState(setState, getState, api) + const api = { setState, getState, getInitialState, subscribe, destroy } + const initialState = (state = createState(setState, getState, api)) return api as any } diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx index a73224c0ee..efc386eb5f 100644 --- a/tests/basic.test.tsx +++ b/tests/basic.test.tsx @@ -31,6 +31,7 @@ it('creates a store hook and api object', () => { [Function], { "destroy": [Function], + "getInitialState": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], diff --git a/tests/ssr.test.tsx b/tests/ssr.test.tsx index c269da2267..08e314ed8f 100644 --- a/tests/ssr.test.tsx +++ b/tests/ssr.test.tsx @@ -63,6 +63,55 @@ describe.skipIf(!React.version.startsWith('18'))( ) }) + const bearCountText = await screen.findByText('bears: 1') + expect(bearCountText).not.toBeNull() + document.body.removeChild(container) + }) + it('should not have hydration errors', async () => { + const useStore = create(() => ({ + bears: 0, + })) + + const { hydrateRoot } = + await vi.importActual( + 'react-dom/client', + ) + + const Component = () => { + const bears = useStore((state) => state.bears) + return
bears: {bears}
+ } + + const markup = renderToString( + Loading...}> + + , + ) + + const container = document.createElement('div') + document.body.appendChild(container) + container.innerHTML = markup + + expect(container.textContent).toContain('bears: 0') + + const consoleMock = vi.spyOn(console, 'error') + + const hydratePromise = act(async () => { + hydrateRoot( + container, + Loading...}> + + , + ) + }) + + // set state during hydration + useStore.setState({ bears: 1 }) + + await hydratePromise + + expect(consoleMock).toHaveBeenCalledTimes(0) + const bearCountText = await screen.findByText('bears: 1') expect(bearCountText).not.toBeNull() document.body.removeChild(container) diff --git a/tests/vanilla/basic.test.ts b/tests/vanilla/basic.test.ts index 9258db17d0..e586860473 100644 --- a/tests/vanilla/basic.test.ts +++ b/tests/vanilla/basic.test.ts @@ -17,12 +17,13 @@ it('create a store', () => { return { value: null } }) expect({ params, result }).toMatchInlineSnapshot(` - { + { "params": [ [Function], [Function], { "destroy": [Function], + "getInitialState": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], @@ -30,6 +31,7 @@ it('create a store', () => { ], "result": { "destroy": [Function], + "getInitialState": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function],