Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: getInitialState #2277

Merged
merged 15 commits into from
Jan 20, 2024
Merged
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 2 additions & 0 deletions src/middleware/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {

let didWarnAboutEqualityFn = false

const identity = <T>(arg: T): T => arg

export function useStore<S extends WithReact<StoreApi<unknown>>>(
api: S,
): ExtractState<S>
Expand All @@ -49,7 +51,7 @@ export function useStore<S extends WithReact<StoreApi<unknown>>, U>(

export function useStore<TState, StateSlice>(
api: WithReact<StoreApi<TState>>,
selector: (state: TState) => StateSlice = api.getState as any,
selector: (state: TState) => StateSlice = identity as any,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
if (
Expand All @@ -65,7 +67,7 @@ export function useStore<TState, StateSlice>(
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState || api.getState,
api.getServerState || api.getInitialState,
selector,
equalityFn,
)
Expand Down
6 changes: 4 additions & 2 deletions src/traditional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
getServerState?: () => ExtractState<S>
}

const identity = <T>(arg: T): T => arg

export function useStoreWithEqualityFn<S extends WithReact<StoreApi<unknown>>>(
api: S,
): ExtractState<S>
Expand All @@ -41,13 +43,13 @@ export function useStoreWithEqualityFn<

export function useStoreWithEqualityFn<TState, StateSlice>(
api: WithReact<StoreApi<TState>>,
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,
)
Expand Down
8 changes: 6 additions & 2 deletions src/vanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type SetStateInternal<T> = {
export interface StoreApi<T> {
setState: SetStateInternal<T>
getState: () => T
getInitialState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
/**
* @deprecated Use `unsubscribe` returned by `subscribe`
Expand Down Expand Up @@ -82,6 +83,9 @@ const createStoreImpl: CreateStoreImpl = (createState) => {

const getState: StoreApi<TState>['getState'] = () => state

const getInitialState: StoreApi<TState>['getInitialState'] = () =>
initialState

const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
listeners.add(listener)
// Unsubscribe
Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions tests/basic.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ it('creates a store hook and api object', () => {
[Function],
{
"destroy": [Function],
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
"subscribe": [Function],
Expand Down
49 changes: 49 additions & 0 deletions tests/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import('react-dom/client')>(
'react-dom/client',
)

const Component = () => {
const bears = useStore((state) => state.bears)
return <div>bears: {bears}</div>
}

const markup = renderToString(
<React.Suspense fallback={<div>Loading...</div>}>
<Component />
</React.Suspense>,
)

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,
<React.Suspense fallback={<div>Loading...</div>}>
<Component />
</React.Suspense>,
)
})

// 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)
Expand Down
4 changes: 3 additions & 1 deletion tests/vanilla/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@ it('create a store', () => {
return { value: null }
})
expect({ params, result }).toMatchInlineSnapshot(`
{
{
"params": [
[Function],
[Function],
{
"destroy": [Function],
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
"subscribe": [Function],
},
],
"result": {
"destroy": [Function],
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
"subscribe": [Function],
Expand Down