Skip to content

Commit

Permalink
Merge branch 'main' into v5
Browse files Browse the repository at this point in the history
  • Loading branch information
dai-shi committed Jan 20, 2024
2 parents af5826b + 13830c1 commit be31b53
Show file tree
Hide file tree
Showing 12 changed files with 689 additions and 790 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test-multiple-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ jobs:
- 18.0.0
- 18.1.0
- 18.2.0
- 18.3.0-canary-6c7b41da3-20231123
- 0.0.0-experimental-6c7b41da3-20231123
- 18.3.0-canary-feed8f3f9-20240118
- 0.0.0-experimental-feed8f3f9-20240118
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
54 changes: 26 additions & 28 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zustand",
"private": true,
"version": "4.4.7",
"version": "4.5.0",
"publishConfig": {
"tag": "next"
},
Expand Down Expand Up @@ -107,49 +107,50 @@
},
"homepage": "https://github.com/pmndrs/zustand",
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/core": "^7.23.7",
"@babel/plugin-external-helpers": "^7.23.3",
"@babel/plugin-transform-react-jsx": "^7.23.4",
"@babel/plugin-transform-runtime": "^7.23.4",
"@babel/plugin-transform-typescript": "^7.23.4",
"@babel/preset-env": "^7.23.3",
"@redux-devtools/extension": "^3.2.6",
"@babel/plugin-transform-runtime": "^7.23.7",
"@babel/plugin-transform-typescript": "^7.23.6",
"@babel/preset-env": "^7.23.8",
"@redux-devtools/extension": "^3.3.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-typescript": "^11.1.5",
"@rollup/plugin-typescript": "^11.1.6",
"@testing-library/react": "^14.1.2",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"@types/node": "^20.11.5",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/use-sync-external-store": "^0.0.6",
"@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/ui": "^0.34.6",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@vitest/coverage-v8": "0.33.0",
"@vitest/ui": "0.33.0",
"concurrently": "^8.2.2",
"esbuild": "^0.19.8",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"esbuild": "^0.19.11",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-vitest": "^0.3.10",
"eslint-plugin-vitest": "^0.3.20",
"immer": "^10.0.3",
"jsdom": "^23.0.0",
"jsdom": "^23.2.0",
"json": "^11.0.0",
"prettier": "^3.1.0",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"redux": "5.0.0-alpha.1",
"rollup": "^4.6.0",
"redux": "^5.0.1",
"rollup": "^4.9.5",
"rollup-plugin-esbuild": "^6.1.0",
"shx": "^0.3.4",
"typescript": "^5.3.2",
"typescript": "^5.3.3",
"use-sync-external-store": "^1.2.0",
"vitest": "^0.34.6"
"vitest": "0.33.0"
},
"peerDependencies": {
"@types/react": ">=18.0",
Expand All @@ -170,8 +171,5 @@
"use-sync-external-store": {
"optional": true
}
},
"resolutions": {
"vite": "4.5.0"
}
}
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 @@ -224,6 +224,8 @@ const persistImpl: 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
7 changes: 5 additions & 2 deletions src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ type ExtractState<S> = S extends { getState: () => infer T } ? T : never
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>

type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
/** @deprecated please use api.getState() */
getServerState?: () => ExtractState<S>
}

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

export function useStore<S extends WithReact<StoreApi<unknown>>>(
api: S,
): ExtractState<S>
Expand All @@ -37,12 +40,12 @@ 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,
) {
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState || api.getState,
api.getServerState || api.getInitialState,
selector,
)
useDebugValue(slice)
Expand Down
7 changes: 5 additions & 2 deletions src/traditional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ type ExtractState<S> = S extends { getState: () => infer T } ? T : never
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>

type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
/** @deprecated please use api.getState() */
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 +44,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
}

Expand Down Expand Up @@ -78,14 +79,17 @@ 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
return () => listeners.delete(listener)
}

const api = { setState, getState, subscribe }
state = createState(setState, getState, api)
const api = { setState, getState, getInitialState, subscribe }
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 @@ -30,6 +30,7 @@ it('creates a store hook and api object', () => {
[Function],
[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,17 +17,19 @@ it('create a store', () => {
return { value: null }
})
expect({ params, result }).toMatchInlineSnapshot(`
{
{
"params": [
[Function],
[Function],
{
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
"subscribe": [Function],
},
],
"result": {
"getInitialState": [Function],
"getState": [Function],
"setState": [Function],
"subscribe": [Function],
Expand Down
7 changes: 4 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true /* FIXME remove this once vite fixes it */,
"allowImportingTsExtensions": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"zustand": ["./src/index.ts"],
"zustand/*": ["./src/*.ts"]
}
"zustand/*": ["./src/*.ts"],
},
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist"],
}
Loading

0 comments on commit be31b53

Please sign in to comment.