From cf511cff525d864bc93459dbed50e21bb43a3f28 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 31 Aug 2021 22:06:44 +0900 Subject: [PATCH 001/144] imaginary code that uses uSES --- package.json | 9 +++-- src/index.ts | 112 +++++---------------------------------------------- yarn.lock | 33 ++++++++------- 3 files changed, 36 insertions(+), 118 deletions(-) diff --git a/package.json b/package.json index 60e6d61bb7..8aec61382d 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,9 @@ "tests/**/*.{js,ts,tsx}" ] }, + "dependencies": { + "use-sync-external-store": "^0.0.0-experimental-45898dacb2-20210828" + }, "devDependencies": { "@babel/core": "^7.15.0", "@babel/plugin-external-helpers": "^7.14.5", @@ -150,8 +153,8 @@ "json": "^11.0.0", "lint-staged": "^11.1.2", "prettier": "^2.3.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "alpha", + "react-dom": "alpha", "rollup": "^2.56.3", "rollup-plugin-esbuild": "^4.5.0", "rollup-plugin-size-snapshot": "^0.12.0", @@ -159,7 +162,7 @@ "typescript": "^4.3.5" }, "peerDependencies": { - "react": ">=16.8" + "react": "alpha" }, "peerDependenciesMeta": { "react": { diff --git a/src/index.ts b/src/index.ts index ad9ef6205b..272b800175 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ -import { useEffect, useLayoutEffect, useReducer, useRef } from 'react' +// @ts-ignore +import { useSyncExternalStoreExtra } from 'use-sync-external-store' import createImpl, { Destroy, EqualityChecker, @@ -12,15 +13,6 @@ import createImpl, { } from './vanilla' export * from './vanilla' -// For server-side rendering: https://github.com/pmndrs/zustand/pull/34 -// Deno support: https://github.com/pmndrs/zustand/issues/347 -const isSSR = - typeof window === 'undefined' || - !window.navigator || - /ServerSideRendering|^Deno\//.test(window.navigator.userAgent) - -const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect - export interface UseStore { (): T (selector: StateSelector, equalityFn?: EqualityChecker): U @@ -37,101 +29,19 @@ export default function create( typeof createState === 'function' ? createImpl(createState) : createState const useStore: any = ( - selector: StateSelector = api.getState as any, - equalityFn: EqualityChecker = Object.is + selector?: StateSelector, + equalityFn?: EqualityChecker ) => { - const [, forceUpdate] = useReducer((c) => c + 1, 0) as [never, () => void] - - const state = api.getState() - const stateRef = useRef(state) - const selectorRef = useRef(selector) - const equalityFnRef = useRef(equalityFn) - const erroredRef = useRef(false) - - const currentSliceRef = useRef() - if (currentSliceRef.current === undefined) { - currentSliceRef.current = selector(state) - } - - let newStateSlice: StateSlice | undefined - let hasNewStateSlice = false - - // The selector or equalityFn need to be called during the render phase if - // they change. We also want legitimate errors to be visible so we re-run - // them if they errored in the subscriber. - if ( - stateRef.current !== state || - selectorRef.current !== selector || - equalityFnRef.current !== equalityFn || - erroredRef.current - ) { - // Using local variables to avoid mutations in the render phase. - newStateSlice = selector(state) - hasNewStateSlice = !equalityFn( - currentSliceRef.current as StateSlice, - newStateSlice - ) - } - - // Syncing changes in useEffect. - useIsomorphicLayoutEffect(() => { - if (hasNewStateSlice) { - currentSliceRef.current = newStateSlice as StateSlice - } - stateRef.current = state - selectorRef.current = selector - equalityFnRef.current = equalityFn - erroredRef.current = false - }) - - const stateBeforeSubscriptionRef = useRef(state) - useIsomorphicLayoutEffect(() => { - const listener = () => { - try { - const nextState = api.getState() - const nextStateSlice = selectorRef.current(nextState) - if ( - !equalityFnRef.current( - currentSliceRef.current as StateSlice, - nextStateSlice - ) - ) { - stateRef.current = nextState - currentSliceRef.current = nextStateSlice - forceUpdate() - } - } catch (error) { - erroredRef.current = true - forceUpdate() - } - } - const unsubscribe = api.subscribe(listener) - if (api.getState() !== stateBeforeSubscriptionRef.current) { - listener() // state has changed before subscription - } - return unsubscribe - }, []) - - return hasNewStateSlice - ? (newStateSlice as StateSlice) - : currentSliceRef.current + const slice = useSyncExternalStoreExtra( + api.subscribe, + api.getState, + selector, + equalityFn + ) + return slice } Object.assign(useStore, api) - // For backward compatibility (No TS types for this) - useStore[Symbol.iterator] = function () { - console.warn( - '[useStore, api] = create() is deprecated and will be removed in v4' - ) - const items = [useStore, api] - return { - next() { - const done = items.length <= 0 - return { value: items.shift(), done } - }, - } - } - return useStore } diff --git a/yarn.lock b/yarn.lock index 81281c278b..1f6c0e554e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1414,7 +1414,7 @@ dependencies: "@types/react" "*" -"@types/react@^17.0.19", "@types/react@*": +"@types/react@*", "@types/react@^17.0.19": version "17.0.19" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.19.tgz#8f2a85e8180a43b57966b237d26a29481dacc991" integrity sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A== @@ -5806,14 +5806,14 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@alpha: + version "18.0.0-alpha-46a0f050a-20210828" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0-alpha-46a0f050a-20210828.tgz#ad2d8f3ea4d41ca1a4738d7ef3a059879c794396" + integrity sha512-qsqu1rS8EWh6R8YmOxSkko2Zac+6mFo42xzuHMs993+sbYjmuThhnZ0eYK7HnQMsSztRZuGoTBmEkC+6W9M88Q== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "0.21.0-alpha-46a0f050a-20210828" react-is@^16.8.1: version "16.13.1" @@ -5825,10 +5825,10 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@alpha: + version "18.0.0-alpha-46a0f050a-20210828" + resolved "https://registry.yarnpkg.com/react/-/react-18.0.0-alpha-46a0f050a-20210828.tgz#a102208ef2f22d945ef0ab31068aa40f3ae35a1c" + integrity sha512-blKyHuOdg1FShz2q9XKnd5hdBYoLpfid5Hl51PrItj/0mBo5vUCRuOXZWeLIvSBwK/zJ1Ldol1+2mWsrY0XifA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -6158,10 +6158,10 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@0.21.0-alpha-46a0f050a-20210828: + version "0.21.0-alpha-46a0f050a-20210828" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0-alpha-46a0f050a-20210828.tgz#ed9b6e1a5489c62d246ef137f8bced3fb30272b7" + integrity sha512-IMtnBFKWebLICSkrXhiiNKfAAC188EYkjcI1iuIYl8TK/TxfM8ua6ReycxS6VldHx4v+mlu8XGbLjeayaYtYcw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -6944,6 +6944,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-sync-external-store@^0.0.0-experimental-45898dacb2-20210828: + version "0.0.0-experimental-45898dacb2-20210828" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-45898dacb2-20210828.tgz#c911c562ab43ffb186ce33e39df66e83ff6c3053" + integrity sha512-ozQMmLoTKtr37o9R02WBUCFqYH+WjnQPuOtwjn24YDLL5xDeq1k4Biy9uMIE/bK+VgOgHKm2N9Ux7nuMhtD6Gg== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From 5570b10b81db6601de320913997b849303bac4eb Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 31 Aug 2021 22:09:09 +0900 Subject: [PATCH 002/144] revert backward compatibility code as this is not going to be v4 --- src/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/index.ts b/src/index.ts index 272b800175..734eb1a35d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,5 +43,19 @@ export default function create( Object.assign(useStore, api) + // For backward compatibility (No TS types for this) + useStore[Symbol.iterator] = function () { + console.warn( + '[useStore, api] = create() is deprecated and will be removed in v4' + ) + const items = [useStore, api] + return { + next() { + const done = items.length <= 0 + return { value: items.shift(), done } + }, + } + } + return useStore } From b03b226eb2c45873c75fa820d4bcf34d89fc505d Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 4 Sep 2021 15:14:15 +0900 Subject: [PATCH 003/144] use use-sync-external-store --- package.json | 2 +- src/index.ts | 4 ++-- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8aec61382d..a6f39a0351 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-45898dacb2-20210828" + "use-sync-external-store": "^0.0.0-experimental-1314299c7-20210901" }, "devDependencies": { "@babel/core": "^7.15.0", diff --git a/src/index.ts b/src/index.ts index 734eb1a35d..eb10a257bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { useSyncExternalStoreExtra } from 'use-sync-external-store' +import { useSyncExternalStoreExtra } from 'use-sync-external-store/extra' import createImpl, { Destroy, EqualityChecker, @@ -29,7 +29,7 @@ export default function create( typeof createState === 'function' ? createImpl(createState) : createState const useStore: any = ( - selector?: StateSelector, + selector: StateSelector = api.getState as any, equalityFn?: EqualityChecker ) => { const slice = useSyncExternalStoreExtra( diff --git a/yarn.lock b/yarn.lock index 1f6c0e554e..2b4afbef73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6944,10 +6944,10 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use-sync-external-store@^0.0.0-experimental-45898dacb2-20210828: - version "0.0.0-experimental-45898dacb2-20210828" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-45898dacb2-20210828.tgz#c911c562ab43ffb186ce33e39df66e83ff6c3053" - integrity sha512-ozQMmLoTKtr37o9R02WBUCFqYH+WjnQPuOtwjn24YDLL5xDeq1k4Biy9uMIE/bK+VgOgHKm2N9Ux7nuMhtD6Gg== +use-sync-external-store@^0.0.0-experimental-1314299c7-20210901: + version "0.0.0-experimental-1314299c7-20210901" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-1314299c7-20210901.tgz#813df01533d654913463becb4baf21d012630b41" + integrity sha512-Iu5x3XZTSw/ORhEu0AUvQc05cLBc7UV8tXiP1zuXJlxEF5dYV+Dmz3YFJHWcb8L+7aBTUIhCfat/qvQNf5LdKw== use@^3.1.0: version "3.1.1" From 811228584323780940acc9586074a82f9b72a3e7 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 4 Sep 2021 15:24:38 +0900 Subject: [PATCH 004/144] revert to react 17 --- package.json | 6 +++--- yarn.lock | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index a6f39a0351..79688f01b6 100644 --- a/package.json +++ b/package.json @@ -153,8 +153,8 @@ "json": "^11.0.0", "lint-staged": "^11.1.2", "prettier": "^2.3.2", - "react": "alpha", - "react-dom": "alpha", + "react": "^17.0.2", + "react-dom": "^17.0.2", "rollup": "^2.56.3", "rollup-plugin-esbuild": "^4.5.0", "rollup-plugin-size-snapshot": "^0.12.0", @@ -162,7 +162,7 @@ "typescript": "^4.3.5" }, "peerDependencies": { - "react": "alpha" + "react": ">=16.8" }, "peerDependenciesMeta": { "react": { diff --git a/yarn.lock b/yarn.lock index 2b4afbef73..d2048d57ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5806,14 +5806,14 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -react-dom@alpha: - version "18.0.0-alpha-46a0f050a-20210828" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0-alpha-46a0f050a-20210828.tgz#ad2d8f3ea4d41ca1a4738d7ef3a059879c794396" - integrity sha512-qsqu1rS8EWh6R8YmOxSkko2Zac+6mFo42xzuHMs993+sbYjmuThhnZ0eYK7HnQMsSztRZuGoTBmEkC+6W9M88Q== +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - scheduler "0.21.0-alpha-46a0f050a-20210828" + scheduler "^0.20.2" react-is@^16.8.1: version "16.13.1" @@ -5825,10 +5825,10 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react@alpha: - version "18.0.0-alpha-46a0f050a-20210828" - resolved "https://registry.yarnpkg.com/react/-/react-18.0.0-alpha-46a0f050a-20210828.tgz#a102208ef2f22d945ef0ab31068aa40f3ae35a1c" - integrity sha512-blKyHuOdg1FShz2q9XKnd5hdBYoLpfid5Hl51PrItj/0mBo5vUCRuOXZWeLIvSBwK/zJ1Ldol1+2mWsrY0XifA== +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -6158,10 +6158,10 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@0.21.0-alpha-46a0f050a-20210828: - version "0.21.0-alpha-46a0f050a-20210828" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0-alpha-46a0f050a-20210828.tgz#ed9b6e1a5489c62d246ef137f8bced3fb30272b7" - integrity sha512-IMtnBFKWebLICSkrXhiiNKfAAC188EYkjcI1iuIYl8TK/TxfM8ua6ReycxS6VldHx4v+mlu8XGbLjeayaYtYcw== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" From 50f2963ba5ad2e8de04c2f704a28d5458b6864ae Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 4 Sep 2021 19:04:05 +0900 Subject: [PATCH 005/144] handle error by our own --- src/index.ts | 40 +++++++++++++++++++++++++++++++--------- tests/basic.test.tsx | 12 +++++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index eb10a257bf..992d13f7fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ +import { useMemo, useState } from 'react' // @ts-ignore -import { useSyncExternalStoreExtra } from 'use-sync-external-store/extra' +import { useSyncExternalStore } from 'use-sync-external-store' import createImpl, { Destroy, EqualityChecker, @@ -30,15 +31,36 @@ export default function create( const useStore: any = ( selector: StateSelector = api.getState as any, - equalityFn?: EqualityChecker + equalityFn: EqualityChecker = Object.is ) => { - const slice = useSyncExternalStoreExtra( - api.subscribe, - api.getState, - selector, - equalityFn - ) - return slice + const [err, setErr] = useState(null) + if (err) { + setErr(null) + throw err + } + const getSnapshot = useMemo(() => { + let lastSnapshot: TState | undefined + let lastSlice: StateSlice | undefined + return () => { + let slice = lastSlice + const snapshot = api.getState() + if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { + try { + slice = selector(snapshot) + if (lastSlice !== undefined && equalityFn(lastSlice, slice)) { + slice = lastSlice + } else { + lastSlice = slice + } + } catch (error) { + setErr(error) + } + lastSnapshot = snapshot + } + return slice + } + }, [selector, equalityFn]) + return useSyncExternalStore(api.subscribe, getSnapshot) } Object.assign(useStore, api) diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx index aaab3e423c..fc30c307c1 100644 --- a/tests/basic.test.tsx +++ b/tests/basic.test.tsx @@ -4,7 +4,7 @@ import { useLayoutEffect, useState, } from 'react' -import { act, fireEvent, render } from '@testing-library/react' +import { act, fireEvent, render, waitFor } from '@testing-library/react' import ReactDOM from 'react-dom' import create, { EqualityChecker, SetState, StateSelector } from '../src/index' @@ -150,7 +150,9 @@ it('re-renders with useLayoutEffect', async () => { const container = document.createElement('div') ReactDOM.render(, container) - expect(container.innerHTML).toBe('true') + waitFor(() => { + expect(container.innerHTML).toBe('true') + }) ReactDOM.unmountComponentAtNode(container) }) @@ -197,14 +199,14 @@ it('can update the selector', async () => { it('can update the equality checker', async () => { type State = { value: number } - type Props = { equalityFn: EqualityChecker } + type Props = { equalityFn: EqualityChecker } const useStore = create(() => ({ value: 0 })) const { setState } = useStore - const selector: StateSelector = (s) => s.value + const selector: StateSelector = (s) => s let renderCount = 0 function Component({ equalityFn }: Props) { - const value = useStore(selector, equalityFn) + const { value } = useStore(selector, equalityFn) return (
renderCount: {++renderCount}, value: {value} From 94d9d4ae3d78c302c678651387fc10e94eebde25 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 4 Sep 2021 19:17:08 +0900 Subject: [PATCH 006/144] v4.0.0-alpha.2 --- .size-snapshot.json | 24 ++++++++++++------------ package.json | 5 ++++- src/index.ts | 14 -------------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 75c6247abf..4fd2d986bb 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,29 +1,29 @@ { "index.js": { - "bundled": 3993, - "minified": 1614, - "gzipped": 786, + "bundled": 2533, + "minified": 927, + "gzipped": 493, "treeshaked": { "rollup": { - "code": 124, - "import_statements": 14 + "code": 46, + "import_statements": 46 }, "webpack": { - "code": 1144 + "code": 1063 } } }, "index.mjs": { - "bundled": 3993, - "minified": 1614, - "gzipped": 786, + "bundled": 2533, + "minified": 927, + "gzipped": 493, "treeshaked": { "rollup": { - "code": 124, - "import_statements": 14 + "code": 46, + "import_statements": 46 }, "webpack": { - "code": 1144 + "code": 1063 } } }, diff --git a/package.json b/package.json index 79688f01b6..061a5cc9c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "zustand", "private": true, - "version": "3.5.10", + "version": "4.0.0-alpha.2", + "publishConfig": { + "tag": "alpha" + }, "description": "🐻 Bear necessities for state management in React", "main": "./index.js", "types": "./index.d.ts", diff --git a/src/index.ts b/src/index.ts index 992d13f7fb..b5c17ea14b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,19 +65,5 @@ export default function create( Object.assign(useStore, api) - // For backward compatibility (No TS types for this) - useStore[Symbol.iterator] = function () { - console.warn( - '[useStore, api] = create() is deprecated and will be removed in v4' - ) - const items = [useStore, api] - return { - next() { - const done = items.length <= 0 - return { value: items.shift(), done } - }, - } - } - return useStore } From d36611b2e89cbfcc9af3c3490f22f99a0795375b Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 6 Sep 2021 12:42:13 +0900 Subject: [PATCH 007/144] fix&refactor a bit --- src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index b5c17ea14b..6bb085a8bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,15 +47,15 @@ export default function create( if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { try { slice = selector(snapshot) - if (lastSlice !== undefined && equalityFn(lastSlice, slice)) { - slice = lastSlice - } else { + if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { lastSlice = slice + } else { + slice = lastSlice } + lastSnapshot = snapshot } catch (error) { setErr(error) } - lastSnapshot = snapshot } return slice } From ef89673a5d8c06b74d8ebb497b3c1d4af35bf5fd Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 14 Sep 2021 11:37:21 +0900 Subject: [PATCH 008/144] update uSES experimental package --- .size-snapshot.json | 12 ++++++------ package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 4fd2d986bb..1fa8bef693 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 2533, - "minified": 927, - "gzipped": 493, + "bundled": 2536, + "minified": 926, + "gzipped": 492, "treeshaked": { "rollup": { "code": 46, @@ -14,9 +14,9 @@ } }, "index.mjs": { - "bundled": 2533, - "minified": 927, - "gzipped": 493, + "bundled": 2536, + "minified": 926, + "gzipped": 492, "treeshaked": { "rollup": { "code": 46, diff --git a/package.json b/package.json index 7b5900db16..6d5966525a 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-1314299c7-20210901" + "use-sync-external-store": "^0.0.0-experimental-fd5e01c2e-20210913" }, "devDependencies": { "@babel/core": "^7.15.0", diff --git a/yarn.lock b/yarn.lock index 531a810047..0748de80a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6944,10 +6944,10 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use-sync-external-store@^0.0.0-experimental-1314299c7-20210901: - version "0.0.0-experimental-1314299c7-20210901" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-1314299c7-20210901.tgz#813df01533d654913463becb4baf21d012630b41" - integrity sha512-Iu5x3XZTSw/ORhEu0AUvQc05cLBc7UV8tXiP1zuXJlxEF5dYV+Dmz3YFJHWcb8L+7aBTUIhCfat/qvQNf5LdKw== +use-sync-external-store@^0.0.0-experimental-fd5e01c2e-20210913: + version "0.0.0-experimental-fd5e01c2e-20210913" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-fd5e01c2e-20210913.tgz#fa6d821e6f710fe9a75926a87a2214882fb56bbd" + integrity sha512-yVlkhz9/tba6Poz3mhx2c48OFv/pINgnDajaLy7Y7fcX6nkA1g2Bq3pxYKMX1DwnvZicXvDFwJT65jO8tbb5sQ== use@^3.1.0: version "3.1.1" From d61a0b7de4c3371e111a70afc931247c107b9b27 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 14 Sep 2021 11:44:31 +0900 Subject: [PATCH 009/144] remove error propagation hack --- src/index.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6bb085a8bf..a99dbf6ed7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo } from 'react' // @ts-ignore import { useSyncExternalStore } from 'use-sync-external-store' import createImpl, { @@ -33,11 +33,6 @@ export default function create( selector: StateSelector = api.getState as any, equalityFn: EqualityChecker = Object.is ) => { - const [err, setErr] = useState(null) - if (err) { - setErr(null) - throw err - } const getSnapshot = useMemo(() => { let lastSnapshot: TState | undefined let lastSlice: StateSlice | undefined @@ -45,17 +40,13 @@ export default function create( let slice = lastSlice const snapshot = api.getState() if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { - try { - slice = selector(snapshot) - if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { - lastSlice = slice - } else { - slice = lastSlice - } - lastSnapshot = snapshot - } catch (error) { - setErr(error) + slice = selector(snapshot) + if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { + lastSlice = slice + } else { + slice = lastSlice } + lastSnapshot = snapshot } return slice } From ec117e68752a643d4835bfc0f2ca6b588ffb1f86 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 14 Sep 2021 11:48:49 +0900 Subject: [PATCH 010/144] update size snapshot --- .size-snapshot.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 1fa8bef693..f8ef06fdf2 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 2536, - "minified": 926, - "gzipped": 492, + "bundled": 2329, + "minified": 851, + "gzipped": 454, "treeshaked": { "rollup": { "code": 46, @@ -14,9 +14,9 @@ } }, "index.mjs": { - "bundled": 2536, - "minified": 926, - "gzipped": 492, + "bundled": 2329, + "minified": 851, + "gzipped": 454, "treeshaked": { "rollup": { "code": 46, From e2081c803dbce3211eb6cb1f109de6e5eda166a9 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 27 Sep 2021 22:49:12 +0900 Subject: [PATCH 011/144] update uSES and add dts --- package.json | 3 ++- src/index.ts | 1 - yarn.lock | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6d5966525a..f41a008515 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-fd5e01c2e-20210913" + "use-sync-external-store": "^0.0.0-experimental-b1a1cb116-20210924" }, "devDependencies": { "@babel/core": "^7.15.0", @@ -138,6 +138,7 @@ "@types/jest": "^27.0.1", "@types/react": "^17.0.19", "@types/react-dom": "^17.0.9", + "@types/use-sync-external-store": "^0.0.0", "@typescript-eslint/eslint-plugin": "^4.29.3", "@typescript-eslint/parser": "^4.29.3", "concurrently": "^6.2.1", diff --git a/src/index.ts b/src/index.ts index a99dbf6ed7..cae06a82dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react' -// @ts-ignore import { useSyncExternalStore } from 'use-sync-external-store' import createImpl, { Destroy, diff --git a/yarn.lock b/yarn.lock index 0748de80a9..7c59a3bd32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1440,6 +1440,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/use-sync-external-store@^0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.0.tgz#ec2ebe41a1288e3d5d80aacc4a4c2873db4aafa8" + integrity sha512-dtA1bGSTAyPAcged3AjG9r3BOUzznqgpZkK8HnVrlOOkZunfKnqJikykxUg3xOrBvgGbROLNptpJLflqp8KvkQ== + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -6944,10 +6949,10 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use-sync-external-store@^0.0.0-experimental-fd5e01c2e-20210913: - version "0.0.0-experimental-fd5e01c2e-20210913" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-fd5e01c2e-20210913.tgz#fa6d821e6f710fe9a75926a87a2214882fb56bbd" - integrity sha512-yVlkhz9/tba6Poz3mhx2c48OFv/pINgnDajaLy7Y7fcX6nkA1g2Bq3pxYKMX1DwnvZicXvDFwJT65jO8tbb5sQ== +use-sync-external-store@^0.0.0-experimental-b1a1cb116-20210924: + version "0.0.0-experimental-b1a1cb116-20210924" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-b1a1cb116-20210924.tgz#9506e0fae23522c106e36d8a3bce6ad9c2859903" + integrity sha512-KMaJzMIlHucOIRFgKcDZdztIjweJxqs6IurFaw61zo17BWQrc6jmZwHX/IpslT7dNqL3xkI/soK2SOGaLPX8LQ== use@^3.1.0: version "3.1.1" From 1fee70e838f5b2c1da9e4345c1d7eb067209f815 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 27 Sep 2021 23:09:46 +0900 Subject: [PATCH 012/144] split react.ts and no export wild --- src/index.ts | 65 +++++++++----------------------------------------- src/react.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++ src/vanilla.ts | 4 +++- 3 files changed, 73 insertions(+), 55 deletions(-) create mode 100644 src/react.ts diff --git a/src/index.ts b/src/index.ts index cae06a82dc..44877f25f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,59 +1,16 @@ -import { useMemo } from 'react' -import { useSyncExternalStore } from 'use-sync-external-store' -import createImpl, { - Destroy, - EqualityChecker, - GetState, - SetState, +export { State, - StateCreator, + PartialState, StateSelector, - StoreApi, + EqualityChecker, + StateListener, + StateSliceListener, Subscribe, + SetState, + GetState, + Destroy, + StoreApi, + StateCreator, } from './vanilla' -export * from './vanilla' - -export interface UseStore { - (): T - (selector: StateSelector, equalityFn?: EqualityChecker): U - setState: SetState - getState: GetState - subscribe: Subscribe - destroy: Destroy -} - -export default function create( - createState: StateCreator | StoreApi -): UseStore { - const api: StoreApi = - typeof createState === 'function' ? createImpl(createState) : createState - - const useStore: any = ( - selector: StateSelector = api.getState as any, - equalityFn: EqualityChecker = Object.is - ) => { - const getSnapshot = useMemo(() => { - let lastSnapshot: TState | undefined - let lastSlice: StateSlice | undefined - return () => { - let slice = lastSlice - const snapshot = api.getState() - if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { - slice = selector(snapshot) - if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { - lastSlice = slice - } else { - slice = lastSlice - } - lastSnapshot = snapshot - } - return slice - } - }, [selector, equalityFn]) - return useSyncExternalStore(api.subscribe, getSnapshot) - } - - Object.assign(useStore, api) - return useStore -} +export { UseStore, create as default } from './react' diff --git a/src/react.ts b/src/react.ts new file mode 100644 index 0000000000..a1cb82700d --- /dev/null +++ b/src/react.ts @@ -0,0 +1,59 @@ +import { useMemo } from 'react' +import { useSyncExternalStore } from 'use-sync-external-store' +import { + Destroy, + EqualityChecker, + GetState, + SetState, + State, + StateCreator, + StateSelector, + StoreApi, + Subscribe, + create as createImpl, +} from './vanilla' + +export interface UseStore { + (): T + (selector: StateSelector, equalityFn?: EqualityChecker): U + setState: SetState + getState: GetState + subscribe: Subscribe + destroy: Destroy +} + +export function create( + createState: StateCreator | StoreApi +): UseStore { + const api: StoreApi = + typeof createState === 'function' ? createImpl(createState) : createState + + const useStore: any = ( + selector: StateSelector = api.getState as any, + equalityFn: EqualityChecker = Object.is + ) => { + const getSnapshot = useMemo(() => { + let lastSnapshot: TState | undefined + let lastSlice: StateSlice | undefined + return () => { + let slice = lastSlice + const snapshot = api.getState() + if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { + slice = selector(snapshot) + if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { + lastSlice = slice + } else { + slice = lastSlice + } + lastSnapshot = snapshot + } + return slice + } + }, [selector, equalityFn]) + return useSyncExternalStore(api.subscribe, getSnapshot) + } + + Object.assign(useStore, api) + + return useStore +} diff --git a/src/vanilla.ts b/src/vanilla.ts index 62f6eb817b..77d19e78a5 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -48,7 +48,7 @@ export type StateCreator> = ( api: StoreApi ) => T -export default function create( +export function create( createState: StateCreator ): StoreApi { let state: TState @@ -112,3 +112,5 @@ export default function create( state = createState(setState, getState, api) return api } + +export default create From 9aaeb5e46e0031c15857384bcd81e571f28403e9 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 27 Sep 2021 23:26:42 +0900 Subject: [PATCH 013/144] split useStore impl --- src/index.ts | 2 +- src/react.ts | 69 ++++++++++++++++++++++++++++---------------------- src/vanilla.ts | 4 +-- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/index.ts b/src/index.ts index 44877f25f2..644b88a8bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,4 +13,4 @@ export { StateCreator, } from './vanilla' -export { UseStore, create as default } from './react' +export { UseStore, default } from './react' diff --git a/src/react.ts b/src/react.ts index a1cb82700d..a5ea405556 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { useSyncExternalStore } from 'use-sync-external-store' -import { +import createApi, { Destroy, EqualityChecker, GetState, @@ -10,9 +10,40 @@ import { StateSelector, StoreApi, Subscribe, - create as createImpl, } from './vanilla' +export function useStore(api: StoreApi): T +export function useStore( + api: StoreApi, + selector: StateSelector, + equalityFn?: EqualityChecker +): U +export function useStore( + api: StoreApi, + selector: StateSelector = api.getState as any, + equalityFn: EqualityChecker = Object.is +) { + const getSnapshot = useMemo(() => { + let lastSnapshot: TState | undefined + let lastSlice: StateSlice | undefined + return () => { + let slice = lastSlice + const snapshot = api.getState() + if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { + slice = selector(snapshot) + if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { + lastSlice = slice + } else { + slice = lastSlice + } + lastSnapshot = snapshot + } + return slice + } + }, [api, selector, equalityFn]) + return useSyncExternalStore(api.subscribe, getSnapshot) +} + export interface UseStore { (): T (selector: StateSelector, equalityFn?: EqualityChecker): U @@ -22,38 +53,16 @@ export interface UseStore { destroy: Destroy } -export function create( +export default function create( createState: StateCreator | StoreApi ): UseStore { const api: StoreApi = - typeof createState === 'function' ? createImpl(createState) : createState + typeof createState === 'function' ? createApi(createState) : createState - const useStore: any = ( - selector: StateSelector = api.getState as any, - equalityFn: EqualityChecker = Object.is - ) => { - const getSnapshot = useMemo(() => { - let lastSnapshot: TState | undefined - let lastSlice: StateSlice | undefined - return () => { - let slice = lastSlice - const snapshot = api.getState() - if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { - slice = selector(snapshot) - if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { - lastSlice = slice - } else { - slice = lastSlice - } - lastSnapshot = snapshot - } - return slice - } - }, [selector, equalityFn]) - return useSyncExternalStore(api.subscribe, getSnapshot) - } + const useStoreImpl: any = (selector?: any, equalityFn?: any) => + useStore(api, selector, equalityFn) - Object.assign(useStore, api) + Object.assign(useStoreImpl, api) - return useStore + return useStoreImpl } diff --git a/src/vanilla.ts b/src/vanilla.ts index 77d19e78a5..fc6d8958eb 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -48,7 +48,7 @@ export type StateCreator> = ( api: StoreApi ) => T -export function create( +export default function createApi( createState: StateCreator ): StoreApi { let state: TState @@ -112,5 +112,3 @@ export function create( state = createState(setState, getState, api) return api } - -export default create From a914052ce5ec3437c49dea53352c003bf45f6cdc Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 28 Sep 2021 00:22:20 +0900 Subject: [PATCH 014/144] context to follow the new api, export wild again --- .size-snapshot.json | 48 +++++++++++++++++++-------------------- src/context.ts | 55 +++++++++++++++++++-------------------------- src/index.ts | 26 +++++++++------------ src/react.ts | 14 ++++++------ src/vanilla.ts | 2 +- 5 files changed, 66 insertions(+), 79 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index f8ef06fdf2..cf0a899237 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 2329, - "minified": 851, - "gzipped": 454, + "bundled": 2419, + "minified": 919, + "gzipped": 471, "treeshaked": { "rollup": { "code": 46, @@ -14,9 +14,9 @@ } }, "index.mjs": { - "bundled": 2329, - "minified": 851, - "gzipped": 454, + "bundled": 2419, + "minified": 919, + "gzipped": 471, "treeshaked": { "rollup": { "code": 46, @@ -28,9 +28,9 @@ } }, "vanilla.js": { - "bundled": 1352, - "minified": 444, - "gzipped": 288, + "bundled": 1362, + "minified": 454, + "gzipped": 293, "treeshaked": { "rollup": { "code": 0, @@ -42,9 +42,9 @@ } }, "vanilla.mjs": { - "bundled": 1352, - "minified": 444, - "gzipped": 288, + "bundled": 1362, + "minified": 454, + "gzipped": 293, "treeshaked": { "rollup": { "code": 0, @@ -112,30 +112,30 @@ } }, "context.js": { - "bundled": 1516, - "minified": 840, - "gzipped": 418, + "bundled": 1236, + "minified": 753, + "gzipped": 358, "treeshaked": { "rollup": { - "code": 14, - "import_statements": 14 + "code": 30, + "import_statements": 30 }, "webpack": { - "code": 998 + "code": 1047 } } }, "context.mjs": { - "bundled": 1516, - "minified": 840, - "gzipped": 418, + "bundled": 1236, + "minified": 753, + "gzipped": 358, "treeshaked": { "rollup": { - "code": 14, - "import_statements": 14 + "code": 30, + "import_statements": 30 }, "webpack": { - "code": 998 + "code": 1047 } } } diff --git a/src/context.ts b/src/context.ts index c0d0e5ff7c..7144b387b4 100644 --- a/src/context.ts +++ b/src/context.ts @@ -6,8 +6,13 @@ import { useMemo, useRef, } from 'react' -import { EqualityChecker, UseStore } from 'zustand' -import { State, StateSelector } from './vanilla' +import { + EqualityChecker, + State, + StateSelector, + StoreApi, + useStore, +} from 'zustand' export interface UseContextStore { (): T @@ -15,33 +20,20 @@ export interface UseContextStore { } function createContext() { - const ZustandContext = reactCreateContext | undefined>( + const ZustandContext = reactCreateContext | undefined>( undefined ) const Provider = ({ - initialStore, createStore, children, }: { - /** - * @deprecated - */ - initialStore?: UseStore - createStore: () => UseStore + createStore: () => StoreApi children: ReactNode }) => { - const storeRef = useRef>() + const storeRef = useRef>() if (!storeRef.current) { - if (initialStore) { - console.warn( - 'Provider initialStore is deprecated and will be removed in the next version.' - ) - if (!createStore) { - createStore = () => initialStore - } - } storeRef.current = createStore() } @@ -52,45 +44,44 @@ function createContext() { ) } - const useStore: UseContextStore = ( + const useBoundStore: UseContextStore = ( selector?: StateSelector, equalityFn = Object.is ) => { - // ZustandContext value is guaranteed to be stable. - const useProviderStore = useContext(ZustandContext) - if (!useProviderStore) { + const store = useContext(ZustandContext) + if (!store) { throw new Error( 'Seems like you have not used zustand provider as an ancestor.' ) } - return useProviderStore( + return useStore( + store, selector as StateSelector, equalityFn ) } const useStoreApi = () => { - // ZustandContext value is guaranteed to be stable. - const useProviderStore = useContext(ZustandContext) - if (!useProviderStore) { + const store = useContext(ZustandContext) + if (!store) { throw new Error( 'Seems like you have not used zustand provider as an ancestor.' ) } return useMemo( () => ({ - getState: useProviderStore.getState, - setState: useProviderStore.setState, - subscribe: useProviderStore.subscribe, - destroy: useProviderStore.destroy, + getState: store.getState, + setState: store.setState, + subscribe: store.subscribe, + destroy: store.destroy, }), - [useProviderStore] + [store] ) } return { Provider, - useStore, + useStore: useBoundStore, useStoreApi, } } diff --git a/src/index.ts b/src/index.ts index 644b88a8bf..1962be0d77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,12 @@ -export { - State, - PartialState, - StateSelector, - EqualityChecker, - StateListener, - StateSliceListener, - Subscribe, - SetState, - GetState, - Destroy, - StoreApi, - StateCreator, -} from './vanilla' +export * from './vanilla' +export { default as createStore } from './vanilla' +export * from './react' +export { default } from './react' -export { UseStore, default } from './react' +// For v3 compatibility +import { UseBoundStore } from './react' +import { State } from './vanilla' +/** + * @deprecated rename to UseBoundStore + */ +export interface UseStore extends UseBoundStore {} diff --git a/src/react.ts b/src/react.ts index a5ea405556..176675627d 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { useSyncExternalStore } from 'use-sync-external-store' -import createApi, { +import createStore, { Destroy, EqualityChecker, GetState, @@ -44,7 +44,7 @@ export function useStore( return useSyncExternalStore(api.subscribe, getSnapshot) } -export interface UseStore { +export interface UseBoundStore { (): T (selector: StateSelector, equalityFn?: EqualityChecker): U setState: SetState @@ -55,14 +55,14 @@ export interface UseStore { export default function create( createState: StateCreator | StoreApi -): UseStore { +): UseBoundStore { const api: StoreApi = - typeof createState === 'function' ? createApi(createState) : createState + typeof createState === 'function' ? createStore(createState) : createState - const useStoreImpl: any = (selector?: any, equalityFn?: any) => + const useBoundStore: any = (selector?: any, equalityFn?: any) => useStore(api, selector, equalityFn) - Object.assign(useStoreImpl, api) + Object.assign(useBoundStore, api) - return useStoreImpl + return useBoundStore } diff --git a/src/vanilla.ts b/src/vanilla.ts index fc6d8958eb..53e3c8f2a2 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -48,7 +48,7 @@ export type StateCreator> = ( api: StoreApi ) => T -export default function createApi( +export default function createStore( createState: StateCreator ): StoreApi { let state: TState From 902145674e65bd20e7b52d90449a831be58c9349 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 28 Sep 2021 00:31:17 +0900 Subject: [PATCH 015/144] v4.0.0-alpha.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f41a008515..3e032b718a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zustand", "private": true, - "version": "4.0.0-alpha.2", + "version": "4.0.0-alpha.3", "publishConfig": { "tag": "alpha" }, From b9a70f2700264c1edc24d902fb98ae805f1acd65 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 28 Sep 2021 11:56:41 +0900 Subject: [PATCH 016/144] add missing await --- tests/basic.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx index a587261895..81107952a8 100644 --- a/tests/basic.test.tsx +++ b/tests/basic.test.tsx @@ -150,7 +150,7 @@ it('re-renders with useLayoutEffect', async () => { const container = document.createElement('div') ReactDOM.render(, container) - waitFor(() => { + await waitFor(() => { expect(container.innerHTML).toBe('true') }) ReactDOM.unmountComponentAtNode(container) From cc391bc3f119ff5f0659da9d9d829704a2cb41f8 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 1 Oct 2021 22:41:32 +0900 Subject: [PATCH 017/144] update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bb45cb83e0..001d631fd1 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-b1a1cb116-20210924" + "use-sync-external-store": "^0.0.0-experimental-7d38e4fd8-20210930" }, "devDependencies": { "@babel/core": "^7.15.5", diff --git a/yarn.lock b/yarn.lock index 19eabc4d3a..35b673e399 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4995,10 +4995,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^0.0.0-experimental-b1a1cb116-20210924: - version "0.0.0-experimental-fd5e01c2e-20210913" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-fd5e01c2e-20210913.tgz#fa6d821e6f710fe9a75926a87a2214882fb56bbd" - integrity sha512-yVlkhz9/tba6Poz3mhx2c48OFv/pINgnDajaLy7Y7fcX6nkA1g2Bq3pxYKMX1DwnvZicXvDFwJT65jO8tbb5sQ== +use-sync-external-store@^0.0.0-experimental-7d38e4fd8-20210930: + version "0.0.0-experimental-7d38e4fd8-20210930" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-7d38e4fd8-20210930.tgz#e19347e9db200a1407772c4322a508c01cc9a7f3" + integrity sha512-WKomTf2T6KUNzwuJRPaRMGHs+cUFkctnV8fBc1kc4GQFOpaxSqkhbfx0MyGgrzVFzuvH08gqg2wU7kRffRLGcw== v8-compile-cache@^2.0.3: version "2.3.0" From bef6f8133192150f30fe065c49da711db16efc61 Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 3 Oct 2021 13:46:16 +0900 Subject: [PATCH 018/144] update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 001d631fd1..a5a64c4518 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-7d38e4fd8-20210930" + "use-sync-external-store": "^0.0.0-experimental-bdd6d5064-20211001" }, "devDependencies": { "@babel/core": "^7.15.5", diff --git a/yarn.lock b/yarn.lock index 35b673e399..0679eb3737 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4995,10 +4995,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^0.0.0-experimental-7d38e4fd8-20210930: - version "0.0.0-experimental-7d38e4fd8-20210930" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-7d38e4fd8-20210930.tgz#e19347e9db200a1407772c4322a508c01cc9a7f3" - integrity sha512-WKomTf2T6KUNzwuJRPaRMGHs+cUFkctnV8fBc1kc4GQFOpaxSqkhbfx0MyGgrzVFzuvH08gqg2wU7kRffRLGcw== +use-sync-external-store@^0.0.0-experimental-bdd6d5064-20211001: + version "0.0.0-experimental-bdd6d5064-20211001" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-bdd6d5064-20211001.tgz#ae127c927cdc2bf92d044b5a6acaf93a45aae45b" + integrity sha512-6G/W/origc7531iWPiNbVwvTySjrr8s7UlYoTLHZAbMiojXGw7y0kBt83nPRXpXImtxhK53abWrvOZqHyEOSsQ== v8-compile-cache@^2.0.3: version "2.3.0" From 54a5b8c96717ce10d579b8c89f1e84c25c0da0e6 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 5 Oct 2021 07:50:44 +0900 Subject: [PATCH 019/144] uses uSES extra! --- package.json | 2 +- src/react.ts | 29 ++++++++--------------------- yarn.lock | 8 ++++---- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a5a64c4518..c44cf13510 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-bdd6d5064-20211001" + "use-sync-external-store": "^0.0.0-experimental-f2c381131-20211004" }, "devDependencies": { "@babel/core": "^7.15.5", diff --git a/src/react.ts b/src/react.ts index 176675627d..4448862310 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,5 +1,4 @@ -import { useMemo } from 'react' -import { useSyncExternalStore } from 'use-sync-external-store' +import { useSyncExternalStoreExtra } from 'use-sync-external-store/extra' import createStore, { Destroy, EqualityChecker, @@ -23,25 +22,13 @@ export function useStore( selector: StateSelector = api.getState as any, equalityFn: EqualityChecker = Object.is ) { - const getSnapshot = useMemo(() => { - let lastSnapshot: TState | undefined - let lastSlice: StateSlice | undefined - return () => { - let slice = lastSlice - const snapshot = api.getState() - if (lastSnapshot === undefined || !Object.is(lastSnapshot, snapshot)) { - slice = selector(snapshot) - if (lastSlice === undefined || !equalityFn(lastSlice, slice)) { - lastSlice = slice - } else { - slice = lastSlice - } - lastSnapshot = snapshot - } - return slice - } - }, [api, selector, equalityFn]) - return useSyncExternalStore(api.subscribe, getSnapshot) + return useSyncExternalStoreExtra( + api.subscribe, + api.getState, + null, + selector, + equalityFn + ) } export interface UseBoundStore { diff --git a/yarn.lock b/yarn.lock index 0679eb3737..912fa8c3cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4995,10 +4995,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^0.0.0-experimental-bdd6d5064-20211001: - version "0.0.0-experimental-bdd6d5064-20211001" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-bdd6d5064-20211001.tgz#ae127c927cdc2bf92d044b5a6acaf93a45aae45b" - integrity sha512-6G/W/origc7531iWPiNbVwvTySjrr8s7UlYoTLHZAbMiojXGw7y0kBt83nPRXpXImtxhK53abWrvOZqHyEOSsQ== +use-sync-external-store@^0.0.0-experimental-f2c381131-20211004: + version "0.0.0-experimental-f2c381131-20211004" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-f2c381131-20211004.tgz#989f37d21ebf3adc451f7d4c12255ff775b75623" + integrity sha512-6WxUV2ijTWaCnKBhM0ICzQyGGXfXM6Xmke9loq7PwnLl7J9xxM2fcxEWz58jVCUtUan5AEgsll19y5YEMiQf6g== v8-compile-cache@^2.0.3: version "2.3.0" From 1ee93aaa3dbf195275fff4eb080fba6080da999b Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 5 Oct 2021 08:05:33 +0900 Subject: [PATCH 020/144] v4.0.0-alpha.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a75ba8a49..f92aa15376 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zustand", "private": true, - "version": "4.0.0-alpha.3", + "version": "4.0.0-alpha.4", "publishConfig": { "tag": "alpha" }, From 002d101479b27ffe4af40cf40818fafca71f60e6 Mon Sep 17 00:00:00 2001 From: daishi Date: Wed, 20 Oct 2021 07:11:02 +0900 Subject: [PATCH 021/144] fix(types): Rename from UseStore to UseBoundStore --- src/context.ts | 10 +++++----- src/index.ts | 14 +++++++++++++- tests/middlewareTypes.test.tsx | 18 +++++++++--------- tests/types.test.tsx | 4 ++-- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/context.ts b/src/context.ts index c0d0e5ff7c..2a8a1eb006 100644 --- a/src/context.ts +++ b/src/context.ts @@ -6,7 +6,7 @@ import { useMemo, useRef, } from 'react' -import { EqualityChecker, UseStore } from 'zustand' +import { EqualityChecker, UseBoundStore } from 'zustand' import { State, StateSelector } from './vanilla' export interface UseContextStore { @@ -15,7 +15,7 @@ export interface UseContextStore { } function createContext() { - const ZustandContext = reactCreateContext | undefined>( + const ZustandContext = reactCreateContext | undefined>( undefined ) @@ -27,11 +27,11 @@ function createContext() { /** * @deprecated */ - initialStore?: UseStore - createStore: () => UseStore + initialStore?: UseBoundStore + createStore: () => UseBoundStore children: ReactNode }) => { - const storeRef = useRef>() + const storeRef = useRef>() if (!storeRef.current) { if (initialStore) { diff --git a/src/index.ts b/src/index.ts index ad9ef6205b..6c4f460655 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,9 @@ const isSSR = const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect +/** + * @deprecated Please use UseBoundStore instead + */ export interface UseStore { (): T (selector: StateSelector, equalityFn?: EqualityChecker): U @@ -30,9 +33,18 @@ export interface UseStore { destroy: Destroy } +export interface UseBoundStore { + (): T + (selector: StateSelector, equalityFn?: EqualityChecker): U + setState: SetState + getState: GetState + subscribe: Subscribe + destroy: Destroy +} + export default function create( createState: StateCreator | StoreApi -): UseStore { +): UseBoundStore { const api: StoreApi = typeof createState === 'function' ? createImpl(createState) : createState diff --git a/tests/middlewareTypes.test.tsx b/tests/middlewareTypes.test.tsx index 99724625ab..db9e549950 100644 --- a/tests/middlewareTypes.test.tsx +++ b/tests/middlewareTypes.test.tsx @@ -1,6 +1,6 @@ import { produce } from 'immer' import type { Draft } from 'immer' -import create, { UseStore } from 'zustand' +import create, { UseBoundStore } from 'zustand' import { NamedSet, devtools, persist } from 'zustand/middleware' import { State, StateCreator } from 'zustand/vanilla' @@ -21,7 +21,7 @@ const immer = ( } } -const createSelectorHooks = (store: UseStore) => { +const createSelectorHooks = (store: UseBoundStore) => { const storeAsSelectors = store as unknown as ISelectors storeAsSelectors.use = {} as ISelectors['use'] @@ -32,7 +32,7 @@ const createSelectorHooks = (store: UseStore) => { storeAsSelectors.use[storeKey] = () => store(selector) }) - return store as UseStore & ISelectors + return store as UseBoundStore & ISelectors } interface ITestStateProps { @@ -44,7 +44,7 @@ it('should have correct type when creating store with devtool', () => { const createStoreWithDevtool = ( createState: StateCreator, options = { name: 'prefix' } - ): UseStore & ISelectors => { + ): UseBoundStore & ISelectors => { return createSelectorHooks(create(devtools(createState, options))) } @@ -73,7 +73,7 @@ it('should have correct type when creating store with devtool and immer', () => const createStoreWithImmer = ( createState: TImmerConfig, options = { name: 'prefix' } - ): UseStore & ISelectors => { + ): UseBoundStore & ISelectors => { return createSelectorHooks(create(devtools(immer(createState), options))) } @@ -103,7 +103,7 @@ it('should have correct type when creating store with devtool and persist', () = createState: StateCreator, options = { name: 'prefix' }, persistName = 'persist' - ): UseStore & ISelectors => { + ): UseBoundStore & ISelectors => { return createSelectorHooks( create(devtools(persist(createState, { name: persistName }), options)) ) @@ -154,7 +154,7 @@ it('should have correct type when creating store with persist', () => { const createStoreWithPersist = ( createState: StateCreator, persistName = 'persist' - ): UseStore & ISelectors => { + ): UseBoundStore & ISelectors => { return createSelectorHooks( create(persist(createState, { name: persistName })) ) @@ -184,7 +184,7 @@ it('should have correct type when creating store with persist', () => { it('should have correct type when creating store with immer', () => { const createStoreWithImmer = ( createState: TImmerConfig - ): UseStore & ISelectors => { + ): UseBoundStore & ISelectors => { return createSelectorHooks(create(immer(createState))) } @@ -211,7 +211,7 @@ it('should have correct type when creating store with devtool, persist and immer createState: TImmerConfig, options = { name: 'prefix' }, persistName = 'persist' - ): UseStore & ISelectors => { + ): UseBoundStore & ISelectors => { return createSelectorHooks( create( devtools(persist(immer(createState), { name: persistName }), options) diff --git a/tests/types.test.tsx b/tests/types.test.tsx index e95b044cf3..d088af213e 100644 --- a/tests/types.test.tsx +++ b/tests/types.test.tsx @@ -10,7 +10,7 @@ import create, { StateSelector, StoreApi, Subscribe, - UseStore, + UseBoundStore, } from 'zustand' it('can use exposed types', () => { @@ -83,7 +83,7 @@ it('can use exposed types', () => { _destroy: Destroy, _equalityFn: EqualityChecker, _stateCreator: StateCreator, - _useStore: UseStore + _useStore: UseBoundStore ) { expect(true).toBeTruthy() } From 674e3071de43f3f83c2fa5fcc761c52c35fae590 Mon Sep 17 00:00:00 2001 From: daishi Date: Wed, 20 Oct 2021 07:14:11 +0900 Subject: [PATCH 022/144] breaking(types): drop deprecated UseStore type --- src/index.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6c4f460655..94820cb9a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,18 +21,6 @@ const isSSR = const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect -/** - * @deprecated Please use UseBoundStore instead - */ -export interface UseStore { - (): T - (selector: StateSelector, equalityFn?: EqualityChecker): U - setState: SetState - getState: GetState - subscribe: Subscribe - destroy: Destroy -} - export interface UseBoundStore { (): T (selector: StateSelector, equalityFn?: EqualityChecker): U From 0c641fdc3545b7607db5b1a167f36f982799483a Mon Sep 17 00:00:00 2001 From: daishi Date: Wed, 20 Oct 2021 07:16:38 +0900 Subject: [PATCH 023/144] breaking(core): drop v2 hook compatibility --- src/index.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index ad9ef6205b..694d783a53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -119,19 +119,5 @@ export default function create( Object.assign(useStore, api) - // For backward compatibility (No TS types for this) - useStore[Symbol.iterator] = function () { - console.warn( - '[useStore, api] = create() is deprecated and will be removed in v4' - ) - const items = [useStore, api] - return { - next() { - const done = items.length <= 0 - return { value: items.shift(), done } - }, - } - } - return useStore } From 0037eb382dd1667203ca82ac8576005557872a1c Mon Sep 17 00:00:00 2001 From: daishi Date: Wed, 20 Oct 2021 07:18:49 +0900 Subject: [PATCH 024/144] breaking(middleware): drop deprecated persist options --- src/middleware.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 0d521b7c53..7252e5a703 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -227,18 +227,6 @@ export type PersistOptions< deserialize?: ( str: string ) => StorageValue | Promise> - /** - * Prevent some items from being stored. - * - * @deprecated This options is deprecated and will be removed in the next version. Please use the `partialize` option instead. - */ - blacklist?: (keyof S)[] - /** - * Only store the listed properties. - * - * @deprecated This options is deprecated and will be removed in the next version. Please use the `partialize` option instead. - */ - whitelist?: (keyof S)[] /** * Filter the persisted value. * @@ -315,8 +303,6 @@ export const persist = getStorage = () => localStorage, serialize = JSON.stringify as (state: StorageValue) => string, deserialize = JSON.parse as (str: string) => StorageValue>, - blacklist, - whitelist, partialize = (state: S) => state, onRehydrateStorage, version = 0, @@ -327,14 +313,6 @@ export const persist = }), } = options || {} - if (blacklist || whitelist) { - console.warn( - `The ${ - blacklist ? 'blacklist' : 'whitelist' - } option is deprecated and will be removed in the next version. Please use the 'partialize' option instead.` - ) - } - let storage: StateStorage | undefined try { @@ -361,15 +339,6 @@ export const persist = const setItem = (): Thenable => { const state = partialize({ ...get() }) - if (whitelist) { - ;(Object.keys(state) as (keyof S)[]).forEach((key) => { - !whitelist.includes(key) && delete state[key] - }) - } - if (blacklist) { - blacklist.forEach((key) => delete state[key]) - } - let errorInSync: Error | undefined const thenable = thenableSerialize({ state, version }) .then((serializedValue) => From 9b970507bcbd47c50e0761424f560ceb5def3645 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 21 Oct 2021 23:26:11 +0900 Subject: [PATCH 025/144] breaking(core): drop deprecated store.subscribe with selector --- src/vanilla.ts | 50 ++++++-------------------------------------------- 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index f354432269..2775d5de84 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -14,17 +14,9 @@ export type StateSelector = (state: T) => U export type EqualityChecker = (state: T, newState: T) => boolean export type StateListener = (state: T, previousState: T) => void export type StateSliceListener = (slice: T, previousSlice: T) => void -export interface Subscribe { - (listener: StateListener): () => void - /** - * @deprecated Please use `subscribeWithSelector` middleware - */ - ( - listener: StateSliceListener, - selector?: StateSelector, - equalityFn?: EqualityChecker - ): () => void -} +export type Subscribe = ( + listener: StateListener +) => () => void export type SetState = { < @@ -86,40 +78,10 @@ export default function create< const getState: GetState = () => state - const subscribeWithSelector = ( - listener: StateSliceListener, - selector: StateSelector = getState as any, - equalityFn: EqualityChecker = Object.is - ) => { - console.warn('[DEPRECATED] Please use `subscribeWithSelector` middleware') - let currentSlice: StateSlice = selector(state) - function listenerToAdd() { - const nextSlice = selector(state) - if (!equalityFn(currentSlice, nextSlice)) { - const previousSlice = currentSlice - listener((currentSlice = nextSlice), previousSlice) - } - } - listeners.add(listenerToAdd) - // Unsubscribe - return () => listeners.delete(listenerToAdd) - } - - const subscribe: Subscribe = ( - listener: StateListener | StateSliceListener, - selector?: StateSelector, - equalityFn?: EqualityChecker - ) => { - if (selector || equalityFn) { - return subscribeWithSelector( - listener as StateSliceListener, - selector, - equalityFn - ) - } - listeners.add(listener as StateListener) + const subscribe: Subscribe = (listener: StateListener) => { + listeners.add(listener) // Unsubscribe - return () => listeners.delete(listener as StateListener) + return () => listeners.delete(listener) } const destroy: Destroy = () => listeners.clear() From 24d9750e84d772a970d54a0d052994dacc43f5a6 Mon Sep 17 00:00:00 2001 From: daishi Date: Wed, 27 Oct 2021 22:50:43 +0900 Subject: [PATCH 026/144] update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 29adb99726..2a942502c4 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-f2c381131-20211004" + "use-sync-external-store": "^0.0.0-experimental-4298ddbc5-20211023" }, "devDependencies": { "@babel/core": "^7.15.8", diff --git a/yarn.lock b/yarn.lock index 94c2624e72..69a9579f14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4805,10 +4805,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^0.0.0-experimental-f2c381131-20211004: - version "0.0.0-experimental-f2c381131-20211004" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-f2c381131-20211004.tgz#989f37d21ebf3adc451f7d4c12255ff775b75623" - integrity sha512-6WxUV2ijTWaCnKBhM0ICzQyGGXfXM6Xmke9loq7PwnLl7J9xxM2fcxEWz58jVCUtUan5AEgsll19y5YEMiQf6g== +use-sync-external-store@^0.0.0-experimental-4298ddbc5-20211023: + version "0.0.0-experimental-45898dacb2-20210828" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-45898dacb2-20210828.tgz#c911c562ab43ffb186ce33e39df66e83ff6c3053" + integrity sha512-ozQMmLoTKtr37o9R02WBUCFqYH+WjnQPuOtwjn24YDLL5xDeq1k4Biy9uMIE/bK+VgOgHKm2N9Ux7nuMhtD6Gg== v8-compile-cache@^2.0.3: version "2.3.0" From f75e8686ae34bfabf34d40a753bcda99c5404639 Mon Sep 17 00:00:00 2001 From: daishi Date: Wed, 27 Oct 2021 23:28:20 +0900 Subject: [PATCH 027/144] fix update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2a942502c4..6cfbe9fd7a 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "^0.0.0-experimental-4298ddbc5-20211023" + "use-sync-external-store": "0.0.0-experimental-4298ddbc5-20211023" }, "devDependencies": { "@babel/core": "^7.15.8", diff --git a/yarn.lock b/yarn.lock index 69a9579f14..7e232d3cff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4805,10 +4805,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^0.0.0-experimental-4298ddbc5-20211023: - version "0.0.0-experimental-45898dacb2-20210828" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-45898dacb2-20210828.tgz#c911c562ab43ffb186ce33e39df66e83ff6c3053" - integrity sha512-ozQMmLoTKtr37o9R02WBUCFqYH+WjnQPuOtwjn24YDLL5xDeq1k4Biy9uMIE/bK+VgOgHKm2N9Ux7nuMhtD6Gg== +use-sync-external-store@0.0.0-experimental-4298ddbc5-20211023: + version "0.0.0-experimental-4298ddbc5-20211023" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-4298ddbc5-20211023.tgz#5a3c593a58e5eb47daa7b58485a082d6315701d6" + integrity sha512-jytSdDiapE0a5TPZoe2gxKmCKUj8UEXI0EStlGXWBBBhghRAFLoDezRmRv3gBS1IdsvSvHR+Rec9xJXN7bMwPw== v8-compile-cache@^2.0.3: version "2.3.0" From 2ed60ebc9e2fc8964db82a87d42602adedfa4e94 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 28 Oct 2021 07:27:22 +0900 Subject: [PATCH 028/144] v4.0.0-alpha.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cfbe9fd7a..521ec079ef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zustand", "private": true, - "version": "4.0.0-alpha.4", + "version": "4.0.0-alpha.5", "publishConfig": { "tag": "alpha" }, From 3bb2fef9ef22b07241f3b8cffdcd2ea85e91ea50 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 29 Oct 2021 20:23:39 +0900 Subject: [PATCH 029/144] combine subscribe type --- src/middleware.ts | 3 +-- src/vanilla.ts | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 240a60a3f3..ced96aad5b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -213,8 +213,7 @@ export const devtools = return initialState } -type SubscribeWithSelector = { - (listener: StateListener): () => void +type SubscribeWithSelector = Subscribe & { ( selector: StateSelector, listener: StateSliceListener, diff --git a/src/vanilla.ts b/src/vanilla.ts index 2775d5de84..ef86f68958 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -14,9 +14,9 @@ export type StateSelector = (state: T) => U export type EqualityChecker = (state: T, newState: T) => boolean export type StateListener = (state: T, previousState: T) => void export type StateSliceListener = (slice: T, previousSlice: T) => void -export type Subscribe = ( - listener: StateListener -) => () => void +export type Subscribe = { + (listener: StateListener): () => void +} export type SetState = { < From ba9d6d0a124c5f07fa62829ce3ead19ebd16182f Mon Sep 17 00:00:00 2001 From: daishi Date: Sun, 31 Oct 2021 13:46:54 +0900 Subject: [PATCH 030/144] intentional undefined type --- src/middleware.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 613e994b42..281d3ebfb6 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -216,7 +216,7 @@ export type StoreApiWithSubscribeWithSelector = StoreApi & { ): () => void } // Note: required for type inference. can we avoid this? - subscribeWithSelectorEnabled: true + _subscribeWithSelectorEnabled: undefined } export const subscribeWithSelector = @@ -248,7 +248,6 @@ export const subscribeWithSelector = } return origSubscribe(listener) }) as any - api.subscribeWithSelectorEnabled = true const initialState = fn(set, get, api) return initialState } From 8fad66766e318cc336630e97f86de4a2abb0507f Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 1 Nov 2021 08:03:51 +0900 Subject: [PATCH 031/144] add useDebugValue --- src/react.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/react.ts b/src/react.ts index 64418d3fbe..6d561abfd8 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,3 +1,4 @@ +import { useDebugValue } from 'react' import { useSyncExternalStoreExtra } from 'use-sync-external-store/extra' import createStore, { EqualityChecker, @@ -20,13 +21,15 @@ export function useStore( selector: StateSelector = api.getState as any, equalityFn: EqualityChecker = Object.is ) { - return useSyncExternalStoreExtra( + const slice = useSyncExternalStoreExtra( api.subscribe, api.getState, null, selector, equalityFn ) + useDebugValue(slice) + return slice } export type UseBoundStore< From 7575357f213edbe12f3c19026a42985b1708baa5 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 1 Nov 2021 08:26:39 +0900 Subject: [PATCH 032/144] update uSES --- .github/workflows/test-multiple-versions.yml | 4 ++-- package.json | 2 +- src/react.ts | 5 +++-- yarn.lock | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-multiple-versions.yml b/.github/workflows/test-multiple-versions.yml index 77c2145338..0e5b673f00 100644 --- a/.github/workflows/test-multiple-versions.yml +++ b/.github/workflows/test-multiple-versions.yml @@ -30,8 +30,8 @@ jobs: react: - 16.8.0 - 17.0.0 - - 18.0.0-alpha-9c8161ba8-20211028 - - 0.0.0-experimental-9c8161ba8-20211028 + - 18.0.0-alpha-6bce0355c-20211031 + - 0.0.0-experimental-6bce0355c-20211031 testing: [default, alpha] exclude: - { react: 16.8.0, testing: alpha } diff --git a/package.json b/package.json index 143661f578..72256ddba9 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "0.0.0-experimental-4298ddbc5-20211023" + "use-sync-external-store": "1.0.0-alpha-6bce0355c-20211031" }, "devDependencies": { "@babel/core": "^7.16.0", diff --git a/src/react.ts b/src/react.ts index 6d561abfd8..a3c31fe27f 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,5 +1,6 @@ import { useDebugValue } from 'react' -import { useSyncExternalStoreExtra } from 'use-sync-external-store/extra' +// @ts-ignore +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' import createStore, { EqualityChecker, GetState, @@ -21,7 +22,7 @@ export function useStore( selector: StateSelector = api.getState as any, equalityFn: EqualityChecker = Object.is ) { - const slice = useSyncExternalStoreExtra( + const slice = useSyncExternalStoreWithSelector( api.subscribe, api.getState, null, diff --git a/yarn.lock b/yarn.lock index 67d02f6cab..67cd92cd07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4855,10 +4855,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@0.0.0-experimental-4298ddbc5-20211023: - version "0.0.0-experimental-4298ddbc5-20211023" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-0.0.0-experimental-4298ddbc5-20211023.tgz#5a3c593a58e5eb47daa7b58485a082d6315701d6" - integrity sha512-jytSdDiapE0a5TPZoe2gxKmCKUj8UEXI0EStlGXWBBBhghRAFLoDezRmRv3gBS1IdsvSvHR+Rec9xJXN7bMwPw== +use-sync-external-store@1.0.0-alpha-6bce0355c-20211031: + version "1.0.0-alpha-6bce0355c-20211031" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-alpha-6bce0355c-20211031.tgz#fe9ca2a354d6e84792aa9272403040796cbd3be0" + integrity sha512-/ajDkTy2FDKWIEOJESxvyVHNihCKsyOPXivgEYW8XGJHsARwwwVOFsDlf+3lCvaXOEY28mPNlE13kC7u+JJp+A== v8-compile-cache@^2.0.3: version "2.3.0" From e51f75aa9188e15d79e9c343e79a7e6785eedaac Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 2 Nov 2021 20:22:17 +0900 Subject: [PATCH 033/144] update uSES types --- package.json | 2 +- src/react.ts | 1 - yarn.lock | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 72256ddba9..cc695e83fa 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "@types/jest": "^27.0.2", "@types/react": "^17.0.33", "@types/react-dom": "^17.0.10", - "@types/use-sync-external-store": "^0.0.0", + "@types/use-sync-external-store": "^0.0.2", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "concurrently": "^6.3.0", diff --git a/src/react.ts b/src/react.ts index a3c31fe27f..52e6493099 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,5 +1,4 @@ import { useDebugValue } from 'react' -// @ts-ignore import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' import createStore, { EqualityChecker, diff --git a/yarn.lock b/yarn.lock index 67cd92cd07..81f7853394 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1409,10 +1409,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/use-sync-external-store@^0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.0.tgz#ec2ebe41a1288e3d5d80aacc4a4c2873db4aafa8" - integrity sha512-dtA1bGSTAyPAcged3AjG9r3BOUzznqgpZkK8HnVrlOOkZunfKnqJikykxUg3xOrBvgGbROLNptpJLflqp8KvkQ== +"@types/use-sync-external-store@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.2.tgz#8341da05beb7dc3b6339eaa8a1b1e8a6e3472175" + integrity sha512-gHIGn4QNww3pIUE97gshEqGP+aFpFAg3hQvdD9JHKqlnM+l0Q86gMAyQ0ZjibXj8QY+HaKP8cpIgxHgXt9zPQQ== "@types/yargs-parser@*": version "20.2.1" From c001de4713473d876c3274e0084ccdd2841cdf8e Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 2 Nov 2021 21:43:08 +0900 Subject: [PATCH 034/144] breaking(middleware): make persist options.removeItem required --- src/middleware.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 69f8c60a62..7740e06e33 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -286,8 +286,7 @@ type DeepPartial = { export type StateStorage = { getItem: (name: string) => string | null | Promise setItem: (name: string, value: string) => void | Promise - // Note: This will be required in v4 - removeItem?: (name: string) => void | Promise + removeItem: (name: string) => void | Promise } type StorageValue = { state: DeepPartial; version?: number } export type PersistOptions< @@ -449,10 +448,6 @@ export const persist = get, api ) - } else if (!storage.removeItem) { - console.warn( - `[zustand persist middleware] The given storage for item '${options.name}' does not contain a 'removeItem' method, which will be required in v4.` - ) } const thenableSerialize = toThenable(options.serialize) @@ -559,7 +554,7 @@ export const persist = } }, clearStorage: () => { - storage?.removeItem?.(options.name) + storage?.removeItem(options.name) }, rehydrate: () => hydrate() as Promise, hasHydrated: () => hasHydrated, From e7adbbfb34e4ab1056ee20097aee762360fad942 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 2 Nov 2021 21:48:46 +0900 Subject: [PATCH 035/144] update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b421fa108c..0b7a2666cf 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "1.0.0-alpha-6bce0355c-20211031" + "use-sync-external-store": "1.0.0-alpha-5cccacd13-20211101" }, "devDependencies": { "@babel/core": "^7.16.0", diff --git a/yarn.lock b/yarn.lock index ba1ea4cccf..23e2ded70f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4855,10 +4855,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.0.0-alpha-6bce0355c-20211031: - version "1.0.0-alpha-6bce0355c-20211031" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-alpha-6bce0355c-20211031.tgz#fe9ca2a354d6e84792aa9272403040796cbd3be0" - integrity sha512-/ajDkTy2FDKWIEOJESxvyVHNihCKsyOPXivgEYW8XGJHsARwwwVOFsDlf+3lCvaXOEY28mPNlE13kC7u+JJp+A== +use-sync-external-store@1.0.0-alpha-5cccacd13-20211101: + version "1.0.0-alpha-5cccacd13-20211101" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-alpha-5cccacd13-20211101.tgz#f8833884764e1445291c67e6364a5965c74c75e7" + integrity sha512-fHBWZJfojgj5xQ+63annym0vZ4tTtJlEAjzhnSdk6B2ptnPeV2OoGSk91kcpchbbaH1JororN4xXDgJ0A4ar2A== v8-compile-cache@^2.0.3: version "2.3.0" From 5812d0ee11aabd4120ee0b659fecba4722cbb2de Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 2 Nov 2021 21:50:51 +0900 Subject: [PATCH 036/144] v4.0.0-alpha.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b7a2666cf..fa3a663217 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zustand", "private": true, - "version": "4.0.0-alpha.5", + "version": "4.0.0-alpha.6", "publishConfig": { "tag": "alpha" }, From f037a2a221c7f93c32f0db58f255991a009dea08 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 4 Nov 2021 10:15:31 +0900 Subject: [PATCH 037/144] fix(readme): remove memoization section which is no longer valid with uSES --- readme.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/readme.md b/readme.md index 670d5e55f9..8111fd8b86 100644 --- a/readme.md +++ b/readme.md @@ -106,23 +106,6 @@ const treats = useStore( ) ``` -## Memoizing selectors - -It is generally recommended to memoize selectors with useCallback. This will prevent unnecessary computations each render. It also allows React to optimize performance in concurrent mode. - -```jsx -const fruit = useStore(useCallback(state => state.fruits[id], [id])) -``` - -If a selector doesn't depend on scope, you can define it outside the render function to obtain a fixed reference without useCallback. - -```jsx -const selector = state => state.berries - -function Component() { - const berries = useStore(selector) -``` - ## Overwriting state The `set` function has a second argument, `false` by default. Instead of merging, it will replace the state model. Be careful not to wipe out parts you rely on, like actions. From 8339d52cf7374266fc8e9be153193dbff93755b5 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 4 Nov 2021 10:27:01 +0900 Subject: [PATCH 038/144] feat(readme): add new createStore/useStore usage --- readme.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 8111fd8b86..e241cffad6 100644 --- a/readme.md +++ b/readme.md @@ -439,7 +439,33 @@ devtools will only log actions from each separated store unlike in a typical *co ## React context -The store created with `create` doesn't require context providers. In some cases, you may want to use contexts for dependency injection or if you want to initialize your store with props from a component. Because the store is a hook, passing it as a normal context value may violate rules of hooks. To avoid misusage, a special `createContext` is provided. +The store created with `create` doesn't require context providers. In some cases, you may want to use contexts for dependency injection or if you want to initialize your store with props from a component. Because the normal store is a hook, passing it as a normal context value may violate rules of hooks. + +The flexible method available since v4 is to use vanilla store. + +```jsx +import { createContext, useContext } from 'react' +import { createStore, useStore } from 'zustand' + +const store = createStore(...) // vanilla store without hooks + +const StoreContext = createContext() + +const App = () => ( + + ... + +) + +const Component = () => { + const store = useContext(StoreContext) + const slice = useStore(store, selector) + ... +} +``` + +Alternatively, a special `createContext` is provided since v3.5, +which avoid misusing the store hook. ```jsx import create from 'zustand' @@ -461,6 +487,7 @@ const Component = () => { ... } ``` +
createContext usage in real components From 37119f52c05ad8967a40acbf53627e00589744bf Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 9 Nov 2021 23:55:40 +0900 Subject: [PATCH 039/144] update useSES --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 649e5d90ce..e47c3fb7c2 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "1.0.0-alpha-5cccacd13-20211101" + "use-sync-external-store": "1.0.0-alpha-327d5c484-20211106" }, "devDependencies": { "@babel/core": "^7.16.0", @@ -142,7 +142,7 @@ "@types/jest": "^27.0.2", "@types/react": "^17.0.34", "@types/react-dom": "^17.0.11", - "@types/use-sync-external-store": "^0.0.2", + "@types/use-sync-external-store": "^0.0.3", "@typescript-eslint/eslint-plugin": "^5.3.1", "@typescript-eslint/parser": "^5.3.1", "concurrently": "^6.3.0", diff --git a/yarn.lock b/yarn.lock index ee6b5d5de1..fde4a60e6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1409,10 +1409,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/use-sync-external-store@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.2.tgz#8341da05beb7dc3b6339eaa8a1b1e8a6e3472175" - integrity sha512-gHIGn4QNww3pIUE97gshEqGP+aFpFAg3hQvdD9JHKqlnM+l0Q86gMAyQ0ZjibXj8QY+HaKP8cpIgxHgXt9zPQQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== "@types/yargs-parser@*": version "20.2.1" @@ -4873,10 +4873,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.0.0-alpha-5cccacd13-20211101: - version "1.0.0-alpha-5cccacd13-20211101" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-alpha-5cccacd13-20211101.tgz#f8833884764e1445291c67e6364a5965c74c75e7" - integrity sha512-fHBWZJfojgj5xQ+63annym0vZ4tTtJlEAjzhnSdk6B2ptnPeV2OoGSk91kcpchbbaH1JororN4xXDgJ0A4ar2A== +use-sync-external-store@1.0.0-alpha-327d5c484-20211106: + version "1.0.0-alpha-327d5c484-20211106" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-alpha-327d5c484-20211106.tgz#5fc66fa4f2063cca6d5200d3752fcfc37a828ace" + integrity sha512-aooJ6dp3B6nxob2CZl58yr1gNQhxq3Rj4yBDsP7ue6RmZ9+S7Z30RP4j5986Mpa3pMmmUjKEb34FqxuiphO1Uw== v8-compile-cache@^2.0.3: version "2.3.0" From 835684a7c909b8ce8efeb90f593e336353010853 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 Nov 2021 06:39:01 +0900 Subject: [PATCH 040/144] update uSES and deps --- .github/workflows/test-multiple-versions.yml | 4 ++-- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-multiple-versions.yml b/.github/workflows/test-multiple-versions.yml index ab310620ef..4993dbf846 100644 --- a/.github/workflows/test-multiple-versions.yml +++ b/.github/workflows/test-multiple-versions.yml @@ -30,8 +30,8 @@ jobs: react: - 16.8.0 - 17.0.0 - - 18.0.0-alpha-327d5c484-20211106 - - 0.0.0-experimental-327d5c484-20211106 + - 18.0.0-beta-96ca8d915-20211115 + - 0.0.0-experimental-96ca8d915-20211115 testing: [default, alpha] exclude: - { react: 16.8.0, testing: alpha } diff --git a/package.json b/package.json index e47c3fb7c2..e9b20950d3 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ ] }, "dependencies": { - "use-sync-external-store": "1.0.0-alpha-327d5c484-20211106" + "use-sync-external-store": "1.0.0-beta-96ca8d915-20211115" }, "devDependencies": { "@babel/core": "^7.16.0", diff --git a/yarn.lock b/yarn.lock index fde4a60e6f..027c7d68c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4873,10 +4873,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.0.0-alpha-327d5c484-20211106: - version "1.0.0-alpha-327d5c484-20211106" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-alpha-327d5c484-20211106.tgz#5fc66fa4f2063cca6d5200d3752fcfc37a828ace" - integrity sha512-aooJ6dp3B6nxob2CZl58yr1gNQhxq3Rj4yBDsP7ue6RmZ9+S7Z30RP4j5986Mpa3pMmmUjKEb34FqxuiphO1Uw== +use-sync-external-store@1.0.0-beta-96ca8d915-20211115: + version "1.0.0-beta-96ca8d915-20211115" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-beta-96ca8d915-20211115.tgz#80777c497f60b57ca23f52de760ff22080814a3c" + integrity sha512-Xkzd7SoZv29jYc95GO0k1v69oSHUdC+4nNaBQ309zvFFyvzYn+I87ee+OD1RKF8SGRwkEJQxjyFzYRcncdpSSg== v8-compile-cache@^2.0.3: version "2.3.0" From e91bde6755eb3a50c7f33f6cbd1c7bd8520d9de7 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 16 Nov 2021 06:44:50 +0900 Subject: [PATCH 041/144] v4.0.0-alpha.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e9b20950d3..82545ff68c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zustand", "private": true, - "version": "4.0.0-alpha.6", + "version": "4.0.0-alpha.7", "publishConfig": { "tag": "alpha" }, From 92802626bd6a08a793124e438ae11d15e90943b1 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 22 Nov 2021 20:48:44 +0900 Subject: [PATCH 042/144] update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 78b63a3bc9..087250b841 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ ] }, "dependencies": { - "use-sync-external-store": "1.0.0-beta-96ca8d915-20211115" + "use-sync-external-store": "1.0.0-beta-149b420f6-20211119" }, "devDependencies": { "@babel/core": "^7.16.0", diff --git a/yarn.lock b/yarn.lock index f68356f8f7..76c63c3a31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4888,10 +4888,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.0.0-beta-96ca8d915-20211115: - version "1.0.0-beta-96ca8d915-20211115" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-beta-96ca8d915-20211115.tgz#80777c497f60b57ca23f52de760ff22080814a3c" - integrity sha512-Xkzd7SoZv29jYc95GO0k1v69oSHUdC+4nNaBQ309zvFFyvzYn+I87ee+OD1RKF8SGRwkEJQxjyFzYRcncdpSSg== +use-sync-external-store@1.0.0-beta-149b420f6-20211119: + version "1.0.0-beta-149b420f6-20211119" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-beta-149b420f6-20211119.tgz#6380d2384fb473eb8c13e6df103d15232bfb65a0" + integrity sha512-28V1mR2DecHmaRU28G4lFpvQFxLBe0UrLUGKiwrJqdD7Tr3EvlTXtztCsSMJN9LLb1KCki7fYW1u+buLI+gtjg== v8-compile-cache@^2.0.3: version "2.3.0" From 303fd82c0cda1143af4c2c3f667942c60be309cf Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 7 Dec 2021 15:52:36 +0900 Subject: [PATCH 043/144] update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3fac29819a..144be2a678 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ ] }, "dependencies": { - "use-sync-external-store": "1.0.0-beta-149b420f6-20211119" + "use-sync-external-store": "1.0.0-beta-12bffc78d-20211206" }, "devDependencies": { "@babel/core": "^7.16.0", diff --git a/yarn.lock b/yarn.lock index 67ca2a7b79..c7e71d558c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4953,10 +4953,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.0.0-beta-149b420f6-20211119: - version "1.0.0-beta-149b420f6-20211119" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-beta-149b420f6-20211119.tgz#6380d2384fb473eb8c13e6df103d15232bfb65a0" - integrity sha512-28V1mR2DecHmaRU28G4lFpvQFxLBe0UrLUGKiwrJqdD7Tr3EvlTXtztCsSMJN9LLb1KCki7fYW1u+buLI+gtjg== +use-sync-external-store@1.0.0-beta-12bffc78d-20211206: + version "1.0.0-beta-12bffc78d-20211206" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-beta-12bffc78d-20211206.tgz#d283917a3e791dc0ba6ea8c6b3904cdafa029260" + integrity sha512-rm2X1XFGwtQq3ooTF/Zsc5UwmTx0ilmVTtHndFTnueFZ5Y8Vm7imy/TorDwCYeHeHDVBjR13UBZ7bIRGkSKeuQ== v8-compile-cache@^2.0.3: version "2.3.0" From 0d4e0d8df38941cbcd08b8d4c104d7d45e39aab3 Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 7 Dec 2021 15:55:54 +0900 Subject: [PATCH 044/144] shave bytes --- src/context.ts | 2 +- src/react.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context.ts b/src/context.ts index 82d0a963c1..85ccdc2235 100644 --- a/src/context.ts +++ b/src/context.ts @@ -49,7 +49,7 @@ function createContext< const useBoundStore: UseContextStore = ( selector?: StateSelector, - equalityFn = Object.is + equalityFn?: EqualityChecker ) => { const store = useContext(ZustandContext) if (!store) { diff --git a/src/react.ts b/src/react.ts index 77a619460c..374cc55e82 100644 --- a/src/react.ts +++ b/src/react.ts @@ -19,7 +19,7 @@ export function useStore( export function useStore( api: StoreApi, selector: StateSelector = api.getState as any, - equalityFn: EqualityChecker = Object.is + equalityFn?: EqualityChecker ) { const slice = useSyncExternalStoreWithSelector( api.subscribe, From 9d8009aa217225c477f1ac045067e5d3688d5fff Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Fri, 24 Dec 2021 02:08:14 +0530 Subject: [PATCH 045/144] vanilla: add higher kinded mutator types --- src/vanilla.ts | 110 ++++++++++++++++++++++--------------------------- 1 file changed, 49 insertions(+), 61 deletions(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index 2732ae211a..ef9f85e7a6 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -1,15 +1,5 @@ export type State = object -// types inspired by setState from React, see: -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/6c49e45842358ba59a508e13130791989911430d/types/react/v16/index.d.ts#L489-L495 -export type PartialState< - T extends State, - K1 extends keyof T = keyof T, - K2 extends keyof T = K1, - K3 extends keyof T = K2, - K4 extends keyof T = K3 -> = - | (Pick | Pick | Pick | Pick | T) - | ((state: T) => Pick | Pick | Pick | Pick | T) +export type PartialState = Partial export type StateSelector = (state: T) => U export type EqualityChecker = (state: T, newState: T) => boolean export type StateListener = (state: T, previousState: T) => void @@ -18,17 +8,13 @@ export type Subscribe = { (listener: StateListener): () => void } -export type SetState = { - < - K1 extends keyof T, - K2 extends keyof T = K1, - K3 extends keyof T = K2, - K4 extends keyof T = K3 - >( - partial: PartialState, - replace?: boolean - ): void -} +export type SetState = < + Nt extends R extends true ? T : Partial, + R extends boolean +>( + partial: Nt | ((state: T) => Nt), + replace?: R +) => void export type GetState = () => T export type Destroy = () => void export type StoreApi = { @@ -37,44 +23,42 @@ export type StoreApi = { subscribe: Subscribe destroy: Destroy } + +export const $$storeMutators = Symbol('$$storeMutators') + export type StateCreator< T extends State, - CustomSetState = SetState, - CustomGetState = GetState, - CustomStoreApi extends StoreApi = StoreApi -> = (set: CustomSetState, get: CustomGetState, api: CustomStoreApi) => T + Mis extends [StoreMutatorIdentifier, unknown][], + Mos extends [StoreMutatorIdentifier, unknown][], + U = T +> = (( + setState: Get, Mis>, 'setState', undefined>, + getState: Get, Mis>, 'getState', undefined>, + store: Mutate, Mis>, + $$storeMutations: Mis +) => U) & { [$$storeMutators]?: Mos } -function createStore< - TState extends State, - CustomSetState, - CustomGetState, - CustomStoreApi extends StoreApi ->( - createState: StateCreator< - TState, - CustomSetState, - CustomGetState, - CustomStoreApi - > -): CustomStoreApi +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface StoreMutators {} +export type StoreMutatorIdentifier = keyof StoreMutators -function createStore( - createState: StateCreator, GetState, any> -): StoreApi +export type Mutate = Ms extends [] + ? S + : Ms extends [[infer Mi, infer Ma], ...infer Mrs] + ? Mutate[Mi & StoreMutatorIdentifier], Mrs> + : never -function createStore< - TState extends State, - CustomSetState, - CustomGetState, - CustomStoreApi extends StoreApi +type Get = K extends keyof T ? T[K] : F + +type Create = < + T extends State, + Mos extends [StoreMutatorIdentifier, unknown][] = [] >( - createState: StateCreator< - TState, - CustomSetState, - CustomGetState, - CustomStoreApi - > -): CustomStoreApi { + initializer: StateCreator +) => Mutate, Mos> + +const create: Create = (createState) => { + type TState = ReturnType let state: TState const listeners: Set> = new Set() @@ -104,12 +88,16 @@ function createStore< const destroy: Destroy = () => listeners.clear() const api = { setState, getState, subscribe, destroy } - state = createState( - setState as unknown as CustomSetState, - getState as unknown as CustomGetState, - api as unknown as CustomStoreApi - ) - return api as unknown as CustomStoreApi + state = createState(setState, getState, api, undefined as any) + return api as any } -export default createStore +export default create + +type CreateWithState = () => < + Mos extends [StoreMutatorIdentifier, unknown][] = [] +>( + initializer: StateCreator +) => Mutate, Mos> + +export const createWithState: CreateWithState = () => create From bcd6e0d0993eb8c7c2a2c0e71b4352c4316599b7 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Fri, 24 Dec 2021 02:08:47 +0530 Subject: [PATCH 046/144] persist: add higher kinded mutator types --- src/middleware/persist.ts | 358 ++++++++++++++++++++------------------ 1 file changed, 191 insertions(+), 167 deletions(-) diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index f15d340ff3..3bac5a44ed 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -1,4 +1,9 @@ -import { GetState, SetState, State, StoreApi } from '../vanilla' +import { + State, + StateCreator, + StoreApi, + StoreMutatorIdentifier, +} from '../vanilla' type DeepPartial = { [P in keyof T]?: DeepPartial @@ -12,10 +17,7 @@ export type StateStorage = { type StorageValue = { state: DeepPartial; version?: number } -export type PersistOptions< - S, - PersistedState extends Partial = Partial -> = { +export type PersistOptions> = { /** Name of the storage (must be unique) */ name: string /** @@ -74,9 +76,9 @@ export type PersistOptions< type PersistListener = (state: S) => void -export type StoreApiWithPersist = StoreApi & { +export type StoreApiWithPersist = StoreApi & { persist: { - setOptions: (options: Partial>) => void + setOptions: (options: Partial>) => void clearStorage: () => void rehydrate: () => Promise hasHydrated: () => boolean @@ -124,188 +126,210 @@ const toThenable = } } -export const persist = - < - S extends State, - CustomSetState extends SetState, - CustomGetState extends GetState, - CustomStoreApi extends StoreApi - >( - config: ( - set: CustomSetState, - get: CustomGetState, - api: CustomStoreApi - ) => S, - baseOptions: PersistOptions - ) => - ( - set: CustomSetState, - get: CustomGetState, - api: CustomStoreApi & StoreApiWithPersist - ): S => { - let options = { - getStorage: () => localStorage, - serialize: JSON.stringify as (state: StorageValue) => string, - deserialize: JSON.parse as (str: string) => StorageValue>, - partialize: (state: S) => state, - version: 0, - merge: (persistedState: any, currentState: S) => ({ - ...currentState, - ...persistedState, - }), - ...baseOptions, - } +type PersistImpl = ( + storeInitializer: PopArgument>, + options: PersistOptions +) => PopArgument> - let hasHydrated = false - const hydrationListeners = new Set>() - const finishHydrationListeners = new Set>() - let storage: StateStorage | undefined - - try { - storage = options.getStorage() - } catch (e) { - // prevent error if the storage is not defined (e.g. when server side rendering a page) - } +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never - if (!storage) { - return config( - ((...args) => { - console.warn( - `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.` - ) - set(...args) - }) as CustomSetState, - get, - api - ) - } +const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { + type S = ReturnType + let options = { + getStorage: () => localStorage, + serialize: JSON.stringify as (state: StorageValue) => string, + deserialize: JSON.parse as (str: string) => StorageValue>, + partialize: (state: S) => state, + version: 0, + merge: (persistedState: any, currentState: S) => ({ + ...currentState, + ...persistedState, + }), + ...baseOptions, + } - const thenableSerialize = toThenable(options.serialize) + let hasHydrated = false + const hydrationListeners = new Set>() + const finishHydrationListeners = new Set>() + let storage: StateStorage | undefined - const setItem = (): Thenable => { - const state = options.partialize({ ...get() }) + try { + storage = options.getStorage() + } catch (e) { + // prevent error if the storage is not defined (e.g. when server side rendering a page) + } - let errorInSync: Error | undefined - const thenable = thenableSerialize({ state, version: options.version }) - .then((serializedValue) => - (storage as StateStorage).setItem(options.name, serializedValue) + if (!storage) { + return config( + (...args) => { + console.warn( + `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.` ) - .catch((e) => { - errorInSync = e - }) - if (errorInSync) { - throw errorInSync - } - return thenable - } - - const savedSetState = api.setState - - api.setState = (state, replace) => { - savedSetState(state, replace) - void setItem() - } - - const configResult = config( - ((...args) => { set(...args) - void setItem() - }) as CustomSetState, + }, get, api ) + } - // 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. - let stateFromStorage: S | undefined + const thenableSerialize = toThenable(options.serialize) - // rehydrate initial state with existing stored state - const hydrate = () => { - if (!storage) return + const setItem = (): Thenable => { + const state = options.partialize({ ...get() }) - hasHydrated = false - hydrationListeners.forEach((cb) => cb(get())) + let errorInSync: Error | undefined + const thenable = thenableSerialize({ state, version: options.version }) + .then((serializedValue) => + (storage as StateStorage).setItem(options.name, serializedValue) + ) + .catch((e) => { + errorInSync = e + }) + if (errorInSync) { + throw errorInSync + } + return thenable + } - const postRehydrationCallback = - options.onRehydrateStorage?.(get()) || undefined + const savedSetState = api.setState - // bind is used to avoid `TypeError: Illegal invocation` error - return toThenable(storage.getItem.bind(storage))(options.name) - .then((storageValue) => { - if (storageValue) { - return options.deserialize(storageValue) - } - }) - .then((deserializedStorageValue) => { - if (deserializedStorageValue) { - if ( - typeof deserializedStorageValue.version === 'number' && - deserializedStorageValue.version !== options.version - ) { - if (options.migrate) { - return options.migrate( - deserializedStorageValue.state, - deserializedStorageValue.version - ) - } - console.error( - `State loaded from storage couldn't be migrated since no migrate function was provided` + api.setState = (state, replace) => { + savedSetState(state, replace) + void setItem() + } + + const configResult = config( + (...args) => { + set(...args) + void setItem() + }, + get, + api + ) + + // 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. + let stateFromStorage: S | undefined + + // rehydrate initial state with existing stored state + const hydrate = () => { + if (!storage) return + + hasHydrated = false + hydrationListeners.forEach((cb) => cb(get())) + + const postRehydrationCallback = + options.onRehydrateStorage?.(get()) || undefined + + // bind is used to avoid `TypeError: Illegal invocation` error + return toThenable(storage.getItem.bind(storage))(options.name) + .then((storageValue) => { + if (storageValue) { + return options.deserialize(storageValue) + } + }) + .then((deserializedStorageValue) => { + if (deserializedStorageValue) { + if ( + typeof deserializedStorageValue.version === 'number' && + deserializedStorageValue.version !== options.version + ) { + if (options.migrate) { + return options.migrate( + deserializedStorageValue.state, + deserializedStorageValue.version ) - } else { - return deserializedStorageValue.state } + console.error( + `State loaded from storage couldn't be migrated since no migrate function was provided` + ) + } else { + return deserializedStorageValue.state } - }) - .then((migratedState) => { - stateFromStorage = options.merge(migratedState as S, configResult) - - set(stateFromStorage as S, true) - return setItem() - }) - .then(() => { - postRehydrationCallback?.(stateFromStorage, undefined) - hasHydrated = true - finishHydrationListeners.forEach((cb) => cb(stateFromStorage as S)) - }) - .catch((e: Error) => { - postRehydrationCallback?.(undefined, e) - }) - } - - api.persist = { - setOptions: (newOptions) => { - options = { - ...options, - ...newOptions, } + }) + .then((migratedState) => { + stateFromStorage = options.merge(migratedState as S, configResult) - if (newOptions.getStorage) { - storage = newOptions.getStorage() - } - }, - clearStorage: () => { - storage?.removeItem(options.name) - }, - rehydrate: () => hydrate() as Promise, - hasHydrated: () => hasHydrated, - onHydrate: (cb) => { - hydrationListeners.add(cb) + set(stateFromStorage as S, true) + return setItem() + }) + .then(() => { + postRehydrationCallback?.(stateFromStorage, undefined) + hasHydrated = true + finishHydrationListeners.forEach((cb) => cb(stateFromStorage as S)) + }) + .catch((e: Error) => { + postRehydrationCallback?.(undefined, e) + }) + } - return () => { - hydrationListeners.delete(cb) - } - }, - onFinishHydration: (cb) => { - finishHydrationListeners.add(cb) + ;(api as StoreApi & StoreApiWithPersist).persist = { + setOptions: (newOptions) => { + options = { + ...options, + ...newOptions, + } - return () => { - finishHydrationListeners.delete(cb) - } - }, - } + if (newOptions.getStorage) { + storage = newOptions.getStorage() + } + }, + clearStorage: () => { + storage?.removeItem(options.name) + }, + rehydrate: () => hydrate() as Promise, + hasHydrated: () => hasHydrated, + onHydrate: (cb) => { + hydrationListeners.add(cb) + + return () => { + hydrationListeners.delete(cb) + } + }, + onFinishHydration: (cb) => { + finishHydrationListeners.add(cb) + + return () => { + finishHydrationListeners.delete(cb) + } + }, + } + + hydrate() - hydrate() + return stateFromStorage || configResult +} + +type Persist = < + T extends State, + Mps extends [StoreMutatorIdentifier, unknown][] = [], + Mcs extends [StoreMutatorIdentifier, unknown][] = [], + U = Partial +>( + initializer: StateCreator, + options?: PersistOptions +) => StateCreator - return stateFromStorage || configResult +const $$persist = Symbol('$$persist') +type $$persist = typeof $$persist + +declare module '../vanilla' { + interface StoreMutators { + [$$persist]: WithPersist } +} + +type WithPersist = S extends { getState: () => infer T } + ? Write, A>> + : never + +type Write = Omit & U +type Cast = T extends U ? T : U + +export const persist = persistImpl as unknown as Persist From 51ec8cf583ebccd1911b383e0fbd851bb5dc7832 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Fri, 24 Dec 2021 02:44:21 +0530 Subject: [PATCH 047/144] persist: try to minimize diff --- src/middleware/persist.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index 3bac5a44ed..8f59b61f3e 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -126,17 +126,6 @@ const toThenable = } } -type PersistImpl = ( - storeInitializer: PopArgument>, - options: PersistOptions -) => PopArgument> - -type PopArgument unknown> = T extends ( - ...a: [...infer A, infer _] -) => infer R - ? (...a: A) => R - : never - const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { type S = ReturnType let options = { @@ -329,6 +318,17 @@ type WithPersist = S extends { getState: () => infer T } ? Write, A>> : never +type PersistImpl = ( + storeInitializer: PopArgument>, + options: PersistOptions +) => PopArgument> + +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never + type Write = Omit & U type Cast = T extends U ? T : U From 41bf7ea35518bae5999e1acadfdec7a7fe7c1c4c Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Fri, 24 Dec 2021 02:57:58 +0530 Subject: [PATCH 048/144] use `PopArgument` in vanilla too --- src/vanilla.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index ef9f85e7a6..c156ff8b3d 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -88,10 +88,20 @@ const create: Create = (createState) => { const destroy: Destroy = () => listeners.clear() const api = { setState, getState, subscribe, destroy } - state = createState(setState, getState, api, undefined as any) + state = (createState as PopArgument)( + setState, + getState, + api + ) return api as any } +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never + export default create type CreateWithState = () => < From 76b1d4cea0f26a3a2bbc84a20c19065cd4f5b2e7 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 24 Dec 2021 16:43:06 +0900 Subject: [PATCH 049/144] update uSES --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8464816e45..a7998068d4 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ ] }, "dependencies": { - "use-sync-external-store": "1.0.0-beta-12bffc78d-20211206" + "use-sync-external-store": "1.0.0-rc.0" }, "devDependencies": { "@babel/core": "^7.16.5", diff --git a/yarn.lock b/yarn.lock index e8e091fa97..30a64a42e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4962,10 +4962,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.0.0-beta-12bffc78d-20211206: - version "1.0.0-beta-12bffc78d-20211206" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-beta-12bffc78d-20211206.tgz#d283917a3e791dc0ba6ea8c6b3904cdafa029260" - integrity sha512-rm2X1XFGwtQq3ooTF/Zsc5UwmTx0ilmVTtHndFTnueFZ5Y8Vm7imy/TorDwCYeHeHDVBjR13UBZ7bIRGkSKeuQ== +use-sync-external-store@1.0.0-rc.0: + version "1.0.0-rc.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-rc.0.tgz#0d8fb7cbc31ddfb3ee01225f6b0a700cf59c449b" + integrity sha512-0U9Xlc2QDFzSGMB0DvcJQL0+DIdxDPJC7mnZlYFbl7wrSrPMcs89X5TVkNB6Dzg618m8lZop+U+J6ow3vq9RAQ== v8-compile-cache@^2.0.3: version "2.3.0" From 847334d8264bac19bcb385cd775fbd672a3d7006 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Thu, 6 Jan 2022 21:53:13 +0530 Subject: [PATCH 050/144] use overloads instead of `createWithState` --- src/vanilla.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index c156ff8b3d..728735c349 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -50,14 +50,24 @@ export type Mutate = Ms extends [] type Get = K extends keyof T ? T[K] : F -type Create = < +type CreateStore = { + ( + initializer: StateCreator + ): Mutate, Mos> + + (): ( + initializer: StateCreator + ) => Mutate, Mos> +} + +type CreateStoreImpl = < T extends State, Mos extends [StoreMutatorIdentifier, unknown][] = [] >( initializer: StateCreator ) => Mutate, Mos> -const create: Create = (createState) => { +const _createStore: CreateStoreImpl = (createState) => { type TState = ReturnType let state: TState const listeners: Set> = new Set() @@ -102,12 +112,9 @@ type PopArgument unknown> = T extends ( ? (...a: A) => R : never -export default create - -type CreateWithState = () => < - Mos extends [StoreMutatorIdentifier, unknown][] = [] ->( - initializer: StateCreator -) => Mutate, Mos> +const createStore = ((f) => { + if (f === undefined) return _createStore + return _createStore(f) +}) as CreateStore -export const createWithState: CreateWithState = () => create +export default createStore From 3e1beaa927a0757071c1e300f92eb3500499188d Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 8 Jan 2022 21:34:49 +0530 Subject: [PATCH 051/144] avoid symbols --- src/vanilla.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index 728735c349..405566db56 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -24,8 +24,6 @@ export type StoreApi = { destroy: Destroy } -export const $$storeMutators = Symbol('$$storeMutators') - export type StateCreator< T extends State, Mis extends [StoreMutatorIdentifier, unknown][], @@ -36,7 +34,7 @@ export type StateCreator< getState: Get, Mis>, 'getState', undefined>, store: Mutate, Mis>, $$storeMutations: Mis -) => U) & { [$$storeMutators]?: Mos } +) => U) & { $$storeMutators?: Mos } // eslint-disable-next-line @typescript-eslint/no-unused-vars export interface StoreMutators {} From 15b478f1bd2b2380b4c80484617cbeedab72f950 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 8 Jan 2022 21:35:07 +0530 Subject: [PATCH 052/144] add new types to middlewares --- src/middleware/combine.ts | 39 +-- src/middleware/devtools.ts | 401 +++++++++++------------- src/middleware/persist.ts | 9 +- src/middleware/redux.ts | 88 ++++-- src/middleware/subscribeWithSelector.ts | 81 +++-- 5 files changed, 305 insertions(+), 313 deletions(-) diff --git a/src/middleware/combine.ts b/src/middleware/combine.ts index 42899b5baf..f82b06b787 100644 --- a/src/middleware/combine.ts +++ b/src/middleware/combine.ts @@ -1,25 +1,18 @@ -import { GetState, SetState, State, StoreApi } from '../vanilla' -import { NamedSet } from './devtools' +import { State, StateCreator, StoreMutatorIdentifier } from '../vanilla' -type Combine = Omit & U +type Combine = < + T extends State, + U extends State, + Mps extends [StoreMutatorIdentifier, unknown][] = [], + Mcs extends [StoreMutatorIdentifier, unknown][] = [] +>( + initialState: T, + additionalStateCreator: StateCreator +) => StateCreator, Mps, Mcs> -export const combine = - ( - initialState: PrimaryState, - create: ( - // Note: NamedSet added for convenience - set: SetState & NamedSet, - get: GetState, - api: StoreApi - ) => SecondaryState - ) => - ( - set: SetState>, - get: GetState>, - api: StoreApi> - ) => - Object.assign( - {}, - initialState, - create(set as any, get as any, api as any) - ) as Combine +type Write = Omit & U + +export const combine: Combine = + (initialState, create) => + (...a) => + Object.assign({}, initialState, (create as any)(...a)) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index bd69eac0a8..f9501a673b 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -1,12 +1,13 @@ -import { GetState, PartialState, SetState, State, StoreApi } from '../vanilla' +import { + PartialState, + SetState, + State, + StateCreator, + StoreApi, + StoreMutatorIdentifier, +} from '../vanilla' type DevtoolsType = { - /** - * @deprecated along with `api.devtools`, `api.devtools.prefix` is deprecated. - * We no longer prefix the actions/names, because the `name` option already - * creates a separate instance of devtools for each store. - */ - prefix: string subscribe: (dispatch: any) => () => void unsubscribe: () => void send: { @@ -17,252 +18,202 @@ type DevtoolsType = { error: (payload: any) => void } -export type NamedSet = { - < - K1 extends keyof T, - K2 extends keyof T = K1, - K3 extends keyof T = K2, - K4 extends keyof T = K3 - >( - partial: PartialState, - replace?: boolean, - name?: string | { type: unknown } - ): void -} - -export type StoreApiWithDevtools = StoreApi & { - setState: NamedSet - /** - * @deprecated `devtools` property on the store is deprecated - * it will be removed in the next major. - * You shouldn't interact with the extension directly. But in case you still want to - * you can patch `window.__REDUX_DEVTOOLS_EXTENSION__` directly - */ - devtools?: DevtoolsType +type Devtools = < + T extends State, + Mps extends [StoreMutatorIdentifier, unknown][] = [], + Mcs extends [StoreMutatorIdentifier, unknown][] = [], + U = Partial +>( + initializer: StateCreator, + options?: DevtoolsOptions +) => StateCreator + +declare module '../vanilla' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface StoreMutators { + 'zustand/devtools': WithDevtools + } } -export const devtools = - < - S extends State, - CustomSetState extends SetState, - CustomGetState extends GetState, - CustomStoreApi extends StoreApi - >( - fn: (set: NamedSet, get: CustomGetState, api: CustomStoreApi) => S, - options?: - | string +interface DevtoolsOptions { + name?: string + anonymousActionType?: string + serialize?: { + options: + | boolean | { - name?: string - anonymousActionType?: string - serialize?: { - options: - | boolean - | { - date?: boolean - regex?: boolean - undefined?: boolean - nan?: boolean - infinity?: boolean - error?: boolean - symbol?: boolean - map?: boolean - set?: boolean - } - } + date?: boolean + regex?: boolean + undefined?: boolean + nan?: boolean + infinity?: boolean + error?: boolean + symbol?: boolean + map?: boolean + set?: boolean } - ) => - ( - set: CustomSetState, - get: CustomGetState, - api: CustomStoreApi & - StoreApiWithDevtools & { - dispatch?: unknown - dispatchFromDevtools?: boolean - } - ): S => { - const devtoolsOptions = - options === undefined - ? { name: undefined, anonymousActionType: undefined } - : typeof options === 'string' - ? { name: options } - : options - - if (typeof window === 'undefined') { - return fn(set, get, api) - } + } +} - const extensionConnector = - (window as any).__REDUX_DEVTOOLS_EXTENSION__ || - (window as any).top.__REDUX_DEVTOOLS_EXTENSION__ +export type WithDevtools = Write< + Extract, + StoreSetStateWithAction +> - if (!extensionConnector) { - if (process.env.NODE_ENV === 'development') { - console.warn( - '[zustand devtools middleware] Please install/enable Redux devtools extension' - ) - } - return fn(set, get, api) +type StoreSetStateWithAction = S extends { + setState: (...a: infer A) => infer R +} + ? { + setState: (...a: [...a: A, actionType?: string | { type: unknown }]) => R } + : never - let extension = Object.create(extensionConnector.connect(devtoolsOptions)) - // We're using `Object.defineProperty` to set `prefix`, so if extensionConnector.connect - // returns the same reference we'd get cannot redefine property prefix error - // hence we `Object.create` to make a new reference +type DevtoolsImpl = ( + storeInitializer: PopArgument>, + options: DevtoolsOptions +) => PopArgument> - let didWarnAboutDevtools = false - Object.defineProperty(api, 'devtools', { - get: () => { - if (!didWarnAboutDevtools) { - console.warn( - '[zustand devtools middleware] `devtools` property on the store is deprecated ' + - 'it will be removed in the next major.\n' + - "You shouldn't interact with the extension directly. But in case you still want to " + - 'you can patch `window.__REDUX_DEVTOOLS_EXTENSION__` directly' - ) - didWarnAboutDevtools = true - } - return extension - }, - set: (value) => { - if (!didWarnAboutDevtools) { - console.warn( - '[zustand devtools middleware] `api.devtools` is deprecated, ' + - 'it will be removed in the next major.\n' + - "You shouldn't interact with the extension directly. But in case you still want to " + - 'you can patch `window.__REDUX_DEVTOOLS_EXTENSION__` directly' - ) - didWarnAboutDevtools = true - } - extension = value - }, - }) +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never - let didWarnAboutPrefix = false - Object.defineProperty(extension, 'prefix', { - get: () => { - if (!didWarnAboutPrefix) { - console.warn( - '[zustand devtools middleware] along with `api.devtools`, `api.devtools.prefix` is deprecated.\n' + - 'We no longer prefix the actions/names' + - devtoolsOptions.name === - undefined - ? ', pass the `name` option to create a separate instance of devtools for each store.' - : ', because the `name` option already creates a separate instance of devtools for each store.' - ) - didWarnAboutPrefix = true - } - return '' - }, - set: () => { - if (!didWarnAboutPrefix) { - console.warn( - '[zustand devtools middleware] along with `api.devtools`, `api.devtools.prefix` is deprecated.\n' + - 'We no longer prefix the actions/names' + - devtoolsOptions.name === - undefined - ? ', pass the `name` option to create a separate instance of devtools for each store.' - : ', because the `name` option already creates a separate instance of devtools for each store.' - ) - didWarnAboutPrefix = true - } - }, - }) +type Write = Omit & U - let isRecording = true - ;(api.setState as NamedSet) = (state, replace, nameOrAction) => { - set(state, replace) - if (!isRecording) return - extension.send( - nameOrAction === undefined - ? { type: devtoolsOptions.anonymousActionType || 'anonymous' } - : typeof nameOrAction === 'string' - ? { type: nameOrAction } - : nameOrAction, - get() - ) - } - const setStateFromDevtools: SetState = (...a) => { - isRecording = false - set(...a) - isRecording = true - } +export type NamedSet = WithDevtools>['setState'] - const initialState = fn(api.setState, get, api) - extension.init(initialState) +export type StoreApiWithDevtools = WithDevtools> - extension.subscribe((message: any) => { - switch (message.type) { - case 'ACTION': - return parseJsonThen<{ type: unknown; state?: PartialState }>( - message.payload, - (action) => { - if (action.type === '__setState') { - setStateFromDevtools(action.state as PartialState) - return - } +const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { + type S = ReturnType - if (!api.dispatchFromDevtools) return - if (typeof api.dispatch !== 'function') return - ;(api.dispatch as any)(action) - } - ) + const devtoolsOptions = + options === undefined + ? { name: undefined, anonymousActionType: undefined } + : typeof options === 'string' + ? { name: options } + : options - case 'DISPATCH': - switch (message.payload.type) { - case 'RESET': - setStateFromDevtools(initialState) - return extension.init(api.getState()) + if (typeof window === 'undefined') { + return fn(set, get, api) + } - case 'COMMIT': - return extension.init(api.getState()) + const extensionConnector = + (window as any).__REDUX_DEVTOOLS_EXTENSION__ || + (window as any).top.__REDUX_DEVTOOLS_EXTENSION__ - case 'ROLLBACK': - return parseJsonThen(message.state, (state) => { - setStateFromDevtools(state) - extension.init(api.getState()) - }) + if (!extensionConnector) { + if (process.env.NODE_ENV === 'development') { + console.warn( + '[zustand devtools middleware] Please install/enable Redux devtools extension' + ) + } + return fn(set, get, api) + } - case 'JUMP_TO_STATE': - case 'JUMP_TO_ACTION': - return parseJsonThen(message.state, (state) => { - setStateFromDevtools(state) - }) + const extension = extensionConnector.connect(devtoolsOptions) as DevtoolsType + + let isRecording = true + ;(api.setState as NamedSet) = (state, replace, nameOrAction) => { + set(state, replace) + if (!isRecording) return + extension.send( + nameOrAction === undefined + ? { type: devtoolsOptions.anonymousActionType || 'anonymous' } + : typeof nameOrAction === 'string' + ? { type: nameOrAction } + : nameOrAction, + get() + ) + } + const setStateFromDevtools: SetState = (...a) => { + isRecording = false + set(...a) + isRecording = true + } - case 'IMPORT_STATE': { - const { nextLiftedState } = message.payload - const lastComputedState = - nextLiftedState.computedStates.slice(-1)[0]?.state - if (!lastComputedState) return - setStateFromDevtools(lastComputedState) - extension.send(null, nextLiftedState) + const initialState = fn(api.setState, get, api) + extension.init(initialState) + + extension.subscribe((message: any) => { + switch (message.type) { + case 'ACTION': + return parseJsonThen<{ type: unknown; state?: PartialState }>( + message.payload, + (action) => { + if (action.type === '__setState') { + setStateFromDevtools(action.state as PartialState) return } - case 'PAUSE_RECORDING': - return (isRecording = !isRecording) + if (!(api as any).dispatchFromDevtools) return + if (typeof (api as any).dispatch !== 'function') return + ;(api as any).dispatch(action) } - return - } - }) + ) - if (api.dispatchFromDevtools && typeof api.dispatch === 'function') { - let didWarnAboutReservedActionType = false - const originalDispatch = api.dispatch - api.dispatch = (...a: any[]) => { - if (a[0].type === '__setState' && !didWarnAboutReservedActionType) { - console.warn( - '[zustand devtools middleware] "__setState" action type is reserved ' + - 'to set state from the devtools. Avoid using it.' - ) - didWarnAboutReservedActionType = true + case 'DISPATCH': + switch (message.payload.type) { + case 'RESET': + setStateFromDevtools(initialState) + return extension.init(api.getState()) + + case 'COMMIT': + return extension.init(api.getState()) + + case 'ROLLBACK': + return parseJsonThen(message.state, (state) => { + setStateFromDevtools(state) + extension.init(api.getState()) + }) + + case 'JUMP_TO_STATE': + case 'JUMP_TO_ACTION': + return parseJsonThen(message.state, (state) => { + setStateFromDevtools(state) + }) + + case 'IMPORT_STATE': { + const { nextLiftedState } = message.payload + const lastComputedState = + nextLiftedState.computedStates.slice(-1)[0]?.state + if (!lastComputedState) return + setStateFromDevtools(lastComputedState) + extension.send(null, nextLiftedState) + return + } + + case 'PAUSE_RECORDING': + return (isRecording = !isRecording) } - ;(originalDispatch as any)(...a) - } + return } - - return initialState + }) + + if ( + (api as any).dispatchFromDevtools && + typeof (api as any).dispatch === 'function' + ) { + let didWarnAboutReservedActionType = false + const originalDispatch = ((api as any).dispatch(api as any).dispatch = ( + ...a: any[] + ) => { + if (a[0].type === '__setState' && !didWarnAboutReservedActionType) { + console.warn( + '[zustand devtools middleware] "__setState" action type is reserved ' + + 'to set state from the devtools. Avoid using it.' + ) + didWarnAboutReservedActionType = true + } + ;(originalDispatch as any)(...a) + }) } + return initialState +} +export const devtools = devtoolsImpl as unknown as Devtools + const parseJsonThen = (stringified: string, f: (parsed: T) => void) => { let parsed: T | undefined try { diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index 8f59b61f3e..41469128da 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -301,16 +301,13 @@ type Persist = < Mcs extends [StoreMutatorIdentifier, unknown][] = [], U = Partial >( - initializer: StateCreator, + initializer: StateCreator, options?: PersistOptions -) => StateCreator - -const $$persist = Symbol('$$persist') -type $$persist = typeof $$persist +) => StateCreator declare module '../vanilla' { interface StoreMutators { - [$$persist]: WithPersist + 'zustand/persist': WithPersist } } diff --git a/src/middleware/redux.ts b/src/middleware/redux.ts index 52f1f4931e..5898e4b727 100644 --- a/src/middleware/redux.ts +++ b/src/middleware/redux.ts @@ -1,38 +1,68 @@ -import { GetState, SetState, State, StoreApi } from '../vanilla' +import { + State, + StateCreator, + StoreApi, + StoreMutatorIdentifier, +} from '../vanilla' import { NamedSet } from './devtools' -type DevtoolsType = { - prefix: string - subscribe: (dispatch: any) => () => void - unsubscribe: () => void - send: (action: string, state: any) => void - init: (state: any) => void - error: (payload: any) => void +type Redux = < + T extends State, + A extends Action, + Cms extends [StoreMutatorIdentifier, unknown][] = [] +>( + reducer: (state: T, action: A) => T, + initialState: T +) => StateCreator>, Cms, [['zustand/redux', A]]> + +declare module '../vanilla' { + interface StoreMutators { + 'zustand/redux': WithRedux + } +} + +type WithRedux = Write, StoreRedux>> + +interface ReduxState { + dispatch: StoreRedux['dispatch'] +} + +interface Action { + type: unknown +} + +interface StoreRedux { + dispatch: (a: A) => A + dispatchFromDevtools: true } +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never + +type Write = Omit & U + export type StoreApiWithRedux< T extends State, A extends { type: unknown } -> = StoreApi A }> & { - dispatch: (a: A) => A - dispatchFromDevtools: boolean -} +> = WithRedux, A> + +type ReduxImpl = ( + reducer: (state: T, action: A) => T, + initialState: T +) => PopArgument, [], []>> -export const redux = - ( - reducer: (state: S, action: A) => S, - initial: S - ) => - ( - set: SetState A }>, - get: GetState A }>, - api: StoreApiWithRedux & { devtools?: DevtoolsType } - ): S & { dispatch: (a: A) => A } => { - api.dispatch = (action: A) => { - ;(set as NamedSet)((state: S) => reducer(state, action), false, action) - return action - } - api.dispatchFromDevtools = true - - return { dispatch: (...a) => api.dispatch(...a), ...initial } +const reduxImpl: ReduxImpl = (reducer, initial) => (set, get, api) => { + type S = typeof initial + type A = Parameters[1] + ;(api as any).dispatch = (action: A) => { + ;(set as NamedSet)((state: S) => reducer(state, action), false, action) + return action } + ;(api as any).dispatchFromDevtools = true + + return { dispatch: (...a) => (api as any).dispatch(...a), ...initial } +} +export const redux = reduxImpl as Redux diff --git a/src/middleware/subscribeWithSelector.ts b/src/middleware/subscribeWithSelector.ts index 2ab585e521..7d6351a498 100644 --- a/src/middleware/subscribeWithSelector.ts +++ b/src/middleware/subscribeWithSelector.ts @@ -1,43 +1,62 @@ import { - EqualityChecker, - GetState, - SetState, State, + StateCreator, StateListener, - StateSelector, - StateSliceListener, StoreApi, + StoreMutatorIdentifier, Subscribe, } from '../vanilla' -export type StoreApiWithSubscribeWithSelector = StoreApi & { - subscribe: Subscribe & { - (listener: StateListener): () => void - ( - selector: StateSelector, - listener: StateSliceListener, - options?: { - equalityFn?: EqualityChecker - fireImmediately?: boolean - } - ): () => void +type SubscribeWithSelector = < + T extends State, + Mps extends [StoreMutatorIdentifier, unknown][] = [], + Mcs extends [StoreMutatorIdentifier, unknown][] = [] +>( + initializer: StateCreator< + T, + [...Mps, ['zustand/subscribeWithSelector', never]], + Mcs + > +) => StateCreator + +declare module '../vanilla' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface StoreMutators { + ['zustand/subscribeWithSelector']: WithSelectorSubscribe } } -export const subscribeWithSelector = - < - S extends State, - CustomSetState extends SetState, - CustomGetState extends GetState, - CustomStoreApi extends StoreApi - >( - fn: (set: CustomSetState, get: CustomGetState, api: CustomStoreApi) => S - ) => - ( - set: CustomSetState, - get: CustomGetState, - api: CustomStoreApi & StoreApiWithSubscribeWithSelector - ): S => { +type WithSelectorSubscribe = S extends { getState: () => infer T } + ? S & StoreSubscribeWithSelector> + : never + +interface StoreSubscribeWithSelector { + subscribe: ( + selector: (state: T) => U, + listener: (selectedState: U, previousSelectedState: U) => void, + options?: { + equalityFn?: (a: U, b: U) => boolean + fireImmediately?: boolean + } + ) => () => void +} + +export type StoreApiWithSubscribeWithSelector = + WithSelectorSubscribe> + +type SubscribeWithSelectorImpl = ( + storeInitializer: PopArgument> +) => PopArgument> + +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never + +const subscribeWithSelectorImpl: SubscribeWithSelectorImpl = + (fn) => (set, get, api) => { + type S = ReturnType const origSubscribe = api.subscribe as Subscribe api.subscribe = ((selector: any, optListener: any, options: any) => { let listener: StateListener = selector // if no selector @@ -60,3 +79,5 @@ export const subscribeWithSelector = const initialState = fn(set, get, api) return initialState } +export const subscribeWithSelector = + subscribeWithSelectorImpl as unknown as SubscribeWithSelector From 1bdaa39328ccc97149fcf01871c7bcfeb651cea1 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 8 Jan 2022 21:50:54 +0530 Subject: [PATCH 053/144] add new types react --- src/react.ts | 70 +++++++++++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/react.ts b/src/react.ts index 374cc55e82..bcf950bbc1 100644 --- a/src/react.ts +++ b/src/react.ts @@ -2,18 +2,17 @@ import { useDebugValue } from 'react' import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' import createStore, { EqualityChecker, - GetState, - SetState, + Mutate, State, StateCreator, StateSelector, StoreApi, + StoreMutatorIdentifier, } from './vanilla' -export function useStore(api: StoreApi): T -export function useStore( - api: StoreApi, - selector: StateSelector, +export function useStore, U = ExtractState>( + api: S, + selector?: StateSelector, U>, equalityFn?: EqualityChecker ): U export function useStore( @@ -32,42 +31,28 @@ export function useStore( return slice } -export type UseBoundStore< - T extends State, - CustomStoreApi extends StoreApi = StoreApi -> = { - (): T - (selector: StateSelector, equalityFn?: EqualityChecker): U -} & CustomStoreApi +type ExtractState = S extends { getState: () => infer T } ? T : never -function create< - TState extends State, - CustomSetState, - CustomGetState, - CustomStoreApi extends StoreApi ->( - createState: - | StateCreator - | CustomStoreApi -): UseBoundStore +type UseBoundStore = (>( + selector?: (state: ExtractState) => U, + equals?: (a: U, b: U) => boolean +) => U) & + S -function create( - createState: - | StateCreator, GetState, any> - | StoreApi -): UseBoundStore> +type Create = { + ( + initializer: StateCreator + ): UseBoundStore, Mos>> + >(store: S): UseBoundStore -function create< - TState extends State, - CustomSetState, - CustomGetState, - CustomStoreApi extends StoreApi ->( - createState: - | StateCreator - | CustomStoreApi -): UseBoundStore { - const api: CustomStoreApi = + (): ( + initializer: StateCreator + ) => UseBoundStore, Mos>> + >(store: S): UseBoundStore +} + +const _create = (createState: StateCreator) => { + const api = typeof createState === 'function' ? createStore(createState) : createState const useBoundStore: any = (selector?: any, equalityFn?: any) => @@ -78,4 +63,11 @@ function create< return useBoundStore } +const create = (( + createState: StateCreator | undefined +) => { + if (!createState) return _create + return _create(createState) +}) as Create + export default create From 5eeeb3c8318e3caf6196c2252bfb546109a30bc7 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 8 Jan 2022 22:29:23 +0530 Subject: [PATCH 054/144] add new types to context --- src/context.ts | 49 +++++++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/context.ts b/src/context.ts index 85ccdc2235..ecf468d4e0 100644 --- a/src/context.ts +++ b/src/context.ts @@ -14,27 +14,24 @@ import { useStore, } from 'zustand' -export type UseContextStore = { - (): T - (selector: StateSelector, equalityFn?: EqualityChecker): U -} +export type UseContextStore = >( + selector?: (state: ExtractState) => U, + equals?: (a: U, b: U) => boolean +) => U + +type ExtractState = S extends { getState: () => infer T } ? T : never -function createContext< - TState extends State, - CustomStoreApi extends StoreApi = StoreApi ->() { - const ZustandContext = reactCreateContext( - undefined - ) +function createContext>() { + const ZustandContext = reactCreateContext(undefined) const Provider = ({ createStore, children, }: { - createStore: () => CustomStoreApi + createStore: () => S children: ReactNode }) => { - const storeRef = useRef() + const storeRef = useRef() if (!storeRef.current) { storeRef.current = createStore() @@ -47,8 +44,8 @@ function createContext< ) } - const useBoundStore: UseContextStore = ( - selector?: StateSelector, + const useBoundStore: UseContextStore = ( + selector?: StateSelector, StateSlice>, equalityFn?: EqualityChecker ) => { const store = useContext(ZustandContext) @@ -59,17 +56,12 @@ function createContext< } return useStore( store, - selector as StateSelector, + selector as StateSelector, StateSlice>, equalityFn ) } - const useStoreApi = (): { - getState: CustomStoreApi['getState'] - setState: CustomStoreApi['setState'] - subscribe: CustomStoreApi['subscribe'] - destroy: CustomStoreApi['destroy'] - } => { + const useStoreApi = (): S => { const store = useContext(ZustandContext) if (!store) { throw new Error( @@ -77,12 +69,13 @@ function createContext< ) } return useMemo( - () => ({ - getState: store.getState, - setState: store.setState, - subscribe: store.subscribe, - destroy: store.destroy, - }), + () => + ({ + getState: store.getState, + setState: store.setState, + subscribe: store.subscribe, + destroy: store.destroy, + } as S), [store] ) } From dcfb92f03359e30380f33f03a8aba9d1c7954147 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 9 Jan 2022 00:08:55 +0530 Subject: [PATCH 055/144] fix persist types --- src/middleware/persist.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index 41469128da..fc7f22e909 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -76,7 +76,7 @@ export type PersistOptions> = { type PersistListener = (state: S) => void -export type StoreApiWithPersist = StoreApi & { +type StorePersist = { persist: { setOptions: (options: Partial>) => void clearStorage: () => void @@ -87,6 +87,11 @@ export type StoreApiWithPersist = StoreApi & { } } +export type StoreApiWithPersist = WithPersist< + StoreApi, + Ps +> + type Thenable = { then( onFulfilled: (value: Value) => V | Promise | Thenable @@ -258,7 +263,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { }) } - ;(api as StoreApi & StoreApiWithPersist).persist = { + ;(api as StoreApi & StorePersist).persist = { setOptions: (newOptions) => { options = { ...options, @@ -312,7 +317,7 @@ declare module '../vanilla' { } type WithPersist = S extends { getState: () => infer T } - ? Write, A>> + ? Write, A>> : never type PersistImpl = ( From b0ea17fa89e56f994d5900e8eeedbbea8e6ec2e3 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 9 Jan 2022 00:09:04 +0530 Subject: [PATCH 056/144] add immer --- src/middleware/immer.ts | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/middleware/immer.ts diff --git a/src/middleware/immer.ts b/src/middleware/immer.ts new file mode 100644 index 0000000000..b85baa8a19 --- /dev/null +++ b/src/middleware/immer.ts @@ -0,0 +1,67 @@ +import { produce } from 'immer' +import type { Draft } from 'immer' +import { State, StateCreator, StoreMutatorIdentifier } from '../vanilla' + +type Immer = < + T extends State, + Mps extends [StoreMutatorIdentifier, unknown][] = [], + Mcs extends [StoreMutatorIdentifier, unknown][] = [] +>( + initializer: StateCreator +) => StateCreator + +declare module 'zustand' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface StoreMutators { + ['zustand/immer']: WithImmer + } +} + +export type WithImmer = S extends { + getState: () => infer T + setState: infer SetState +} + ? Write< + S, + { + setState: SetState extends ( + ...a: [infer _, infer __, ...infer A] + ) => infer Sr + ? , R extends boolean>( + nextStateOrUpdater: Nt | ((state: Draft) => void), + shouldReplace?: R, + ...a: A + ) => Sr + : never + } + > + : never + +type Write = Omit & U + +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never + +type ImmerImpl = ( + storeInitializer: PopArgument> +) => PopArgument> + +const immerImpl: ImmerImpl = (initializer) => (set, get, store) => { + type T = ReturnType + return initializer( + (updater, replace) => { + const nextState = ( + typeof updater === 'function' ? produce(updater as any) : updater + ) as ((s: T) => T) | T | Partial + + return set(nextState as any, replace) + }, + get, + store + ) +} + +export const immer = immerImpl as unknown as Immer From 45b4317e6744e81c273b546d072492f503ba7042 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 9 Jan 2022 00:10:55 +0530 Subject: [PATCH 057/144] migrate middleware type tests --- tests/middlewareTypes.test.tsx | 146 +++++---------------------------- 1 file changed, 19 insertions(+), 127 deletions(-) diff --git a/tests/middlewareTypes.test.tsx b/tests/middlewareTypes.test.tsx index 18b4b8dbba..d9333113bf 100644 --- a/tests/middlewareTypes.test.tsx +++ b/tests/middlewareTypes.test.tsx @@ -1,50 +1,12 @@ -import { produce } from 'immer' -import type { Draft } from 'immer' -import create, { - GetState, - SetState, - State, - StateCreator, - StoreApi, -} from 'zustand' +import create from 'zustand' import { - PersistOptions, - StoreApiWithDevtools, - StoreApiWithPersist, - StoreApiWithSubscribeWithSelector, combine, devtools, persist, redux, subscribeWithSelector, } from 'zustand/middleware' - -const immer = - < - T extends State, - CustomSetState extends SetState, - CustomGetState extends GetState, - CustomStoreApi extends StoreApi - >( - config: StateCreator< - T, - (partial: ((draft: Draft) => void) | T, replace?: boolean) => void, - CustomGetState, - CustomStoreApi - > - ): StateCreator => - (set, get, api) => - config( - (partial, replace) => { - const nextState = - typeof partial === 'function' - ? produce(partial as (state: Draft) => T) - : (partial as T) - return set(nextState, replace) - }, - get, - api - ) +import { immer } from 'zustand/middleware/immer' type CounterState = { count: number @@ -72,7 +34,7 @@ describe('counter state spec (no middleware)', () => { describe('counter state spec (single middleware)', () => { it('immer', () => { - const useStore = create( + const useStore = create()( immer((set, get) => ({ count: 0, inc: () => @@ -118,12 +80,7 @@ describe('counter state spec (single middleware)', () => { }) it('devtools', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithDevtools - >( + const useStore = create()( devtools( (set, get) => ({ count: 0, @@ -146,12 +103,7 @@ describe('counter state spec (single middleware)', () => { }) it('subscribeWithSelector', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector - >( + const useStore = create()( subscribeWithSelector((set, get) => ({ count: 1, inc: () => set({ count: get().count + 1 }, false), @@ -192,12 +144,7 @@ describe('counter state spec (single middleware)', () => { }) it('persist', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithPersist - >( + const useStore = create()( persist( (set, get) => ({ count: 1, @@ -220,7 +167,7 @@ describe('counter state spec (single middleware)', () => { }) it('persist without custom api (#638)', () => { - const useStore = create( + const useStore = create()( persist( (set, get) => ({ count: 1, @@ -244,12 +191,7 @@ describe('counter state spec (single middleware)', () => { describe('counter state spec (double middleware)', () => { it('devtools & immer', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithDevtools - >( + const useStore = create()( devtools( immer((set, get) => ({ count: 0, @@ -277,8 +219,8 @@ describe('counter state spec (double middleware)', () => { it('devtools & redux', () => { const useStore = create( devtools( - redux<{ count: number }, { type: 'INC' }>( - (state, action) => { + redux( + (state, action: { type: 'INC' }) => { switch (action.type) { case 'INC': return { ...state, count: state.count + 1 } @@ -349,13 +291,7 @@ describe('counter state spec (double middleware)', () => { }) it('devtools & subscribeWithSelector', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector & - StoreApiWithDevtools - >( + const useStore = create()( devtools( subscribeWithSelector((set, get) => ({ count: 1, @@ -382,12 +318,7 @@ describe('counter state spec (double middleware)', () => { }) it('devtools & persist', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithPersist & StoreApiWithDevtools - >( + const useStore = create()( devtools( persist( (set, get) => ({ @@ -416,12 +347,7 @@ describe('counter state spec (double middleware)', () => { describe('counter state spec (triple middleware)', () => { it('devtools & persist & immer', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithPersist & StoreApiWithDevtools - >( + const useStore = create()( devtools( persist( immer((set, get) => ({ @@ -479,14 +405,7 @@ describe('counter state spec (triple middleware)', () => { }) it('devtools & subscribeWithSelector & persist', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector & - StoreApiWithPersist & - StoreApiWithDevtools - >( + const useStore = create()( devtools( subscribeWithSelector( persist( @@ -521,14 +440,7 @@ describe('counter state spec (triple middleware)', () => { describe('counter state spec (quadruple middleware)', () => { it('devtools & subscribeWithSelector & persist & immer (#616)', () => { - const useStore = create< - CounterState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector & - StoreApiWithPersist & - StoreApiWithDevtools - >( + const useStore = create()( devtools( subscribeWithSelector( persist( @@ -566,19 +478,9 @@ describe('counter state spec (quadruple middleware)', () => { describe('more complex state spec with subscribeWithSelector', () => { it('#619, #632', () => { - type MyState = { - foo: boolean - } const useStore = create( subscribeWithSelector( - // NOTE: Adding type annotation to inner middleware works. - persist< - MyState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector & - StoreApiWithPersist - >( + persist( () => ({ foo: true, }), @@ -604,12 +506,7 @@ describe('more complex state spec with subscribeWithSelector', () => { type MyState = { foo: number | null } - const useStore = create< - MyState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector - >( + const useStore = create()( subscribeWithSelector( () => ({ @@ -636,13 +533,8 @@ describe('more complex state spec with subscribeWithSelector', () => { authenticated: boolean authenticate: (username: string, password: string) => Promise } - // NOTE: This is a simplified middleware type without persist api - type MyPersist = ( - config: StateCreator, - options: PersistOptions - ) => StateCreator - const useStore = create( - (persist as MyPersist)( + const useStore = create()( + persist( (set) => ({ token: undefined, authenticated: false, From 40d8ce87a9bc05f907ef4a79cf6ed0ce9184eafb Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 9 Jan 2022 00:18:58 +0530 Subject: [PATCH 058/144] fix react type, export `UseBoundStore` --- src/react.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react.ts b/src/react.ts index bcf950bbc1..6a62b03192 100644 --- a/src/react.ts +++ b/src/react.ts @@ -33,7 +33,7 @@ export function useStore( type ExtractState = S extends { getState: () => infer T } ? T : never -type UseBoundStore = (>( +export type UseBoundStore = (>( selector?: (state: ExtractState) => U, equals?: (a: U, b: U) => boolean ) => U) & From 308112acdaac82ac7bb60565b0d772b3475e42d6 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 9 Jan 2022 00:19:16 +0530 Subject: [PATCH 059/144] migrate vanilla type tests --- src/vanilla.ts | 4 +++- tests/types.test.tsx | 17 ++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index 405566db56..1dc385dcdd 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -1,5 +1,7 @@ export type State = object -export type PartialState = Partial +export type PartialState = + | Partial + | ((state: T) => Partial) export type StateSelector = (state: T) => U export type EqualityChecker = (state: T, newState: T) => boolean export type StateListener = (state: T, previousState: T) => void diff --git a/tests/types.test.tsx b/tests/types.test.tsx index d088af213e..41dc199c1d 100644 --- a/tests/types.test.tsx +++ b/tests/types.test.tsx @@ -30,11 +30,11 @@ it('can use exposed types', () => { } } const selector: StateSelector = (state) => state.num - const partial: PartialState = { + const partial: PartialState = { num: 2, numGet: () => 2, } - const partialFn: PartialState = (state) => ({ + const partialFn: PartialState = (state) => ({ ...state, num: 2, }) @@ -59,7 +59,7 @@ it('can use exposed types', () => { })) const useStore = storeApi - const stateCreator: StateCreator = (set, get) => ({ + const stateCreator: StateCreator = (set, get) => ({ num: 1, numGet: () => get().num, numGetState: () => get().num, @@ -73,7 +73,7 @@ it('can use exposed types', () => { function checkAllTypes( _getState: GetState, - _partialState: PartialState, + _partialState: PartialState, _setState: SetState, _state: State, _stateListener: StateListener, @@ -82,8 +82,8 @@ it('can use exposed types', () => { _subscribe: Subscribe, _destroy: Destroy, _equalityFn: EqualityChecker, - _stateCreator: StateCreator, - _useStore: UseBoundStore + _stateCreator: StateCreator, + _useStore: UseBoundStore> ) { expect(true).toBeTruthy() } @@ -128,7 +128,6 @@ it('should have correct (partial) types for setState', () => { // ok, should not error store.setState({ count: 1 }) store.setState({}) - store.setState(() => undefined) store.setState((previous) => previous) // @ts-expect-error type undefined is not assignable to type number @@ -155,7 +154,7 @@ it('should allow for different partial keys to be returnable from setState', () } return { count: 0 } }) - store.setState<'count', 'something'>((previous) => { + store.setState((previous) => { if (previous.count === 0) { return { count: 1 } } @@ -166,7 +165,7 @@ it('should allow for different partial keys to be returnable from setState', () }) // @ts-expect-error Type '{ something: boolean; count?: undefined; }' is not assignable to type 'State'. - store.setState<'count', 'something'>((previous) => { + store.setState((previous) => { if (previous.count === 0) { return { count: 1 } } From 272e2542538868d5390791ddc2eb45240dfc56d4 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 9 Jan 2022 20:36:23 +0530 Subject: [PATCH 060/144] rename `_createStore` to `createStoreImpl` --- src/vanilla.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index 1dc385dcdd..d0140521a2 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -67,7 +67,7 @@ type CreateStoreImpl = < initializer: StateCreator ) => Mutate, Mos> -const _createStore: CreateStoreImpl = (createState) => { +const createStoreImpl: CreateStoreImpl = (createState) => { type TState = ReturnType let state: TState const listeners: Set> = new Set() @@ -113,8 +113,8 @@ type PopArgument unknown> = T extends ( : never const createStore = ((f) => { - if (f === undefined) return _createStore - return _createStore(f) + if (f === undefined) return createStoreImpl + return createStoreImpl(f) }) as CreateStore export default createStore From c61e2075cd32bc5c6e0b5c0c8c24469486a8d37f Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 17 Jan 2022 03:31:00 +0530 Subject: [PATCH 061/144] Default to no mutations in `StateCreator` --- src/vanilla.ts | 4 ++-- tests/types.test.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vanilla.ts b/src/vanilla.ts index d0140521a2..74d702d020 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -28,8 +28,8 @@ export type StoreApi = { export type StateCreator< T extends State, - Mis extends [StoreMutatorIdentifier, unknown][], - Mos extends [StoreMutatorIdentifier, unknown][], + Mis extends [StoreMutatorIdentifier, unknown][] = [], + Mos extends [StoreMutatorIdentifier, unknown][] = [], U = T > = (( setState: Get, Mis>, 'setState', undefined>, diff --git a/tests/types.test.tsx b/tests/types.test.tsx index 41dc199c1d..2843d4fd6a 100644 --- a/tests/types.test.tsx +++ b/tests/types.test.tsx @@ -59,7 +59,7 @@ it('can use exposed types', () => { })) const useStore = storeApi - const stateCreator: StateCreator = (set, get) => ({ + const stateCreator: StateCreator = (set, get) => ({ num: 1, numGet: () => get().num, numGetState: () => get().num, @@ -82,7 +82,7 @@ it('can use exposed types', () => { _subscribe: Subscribe, _destroy: Destroy, _equalityFn: EqualityChecker, - _stateCreator: StateCreator, + _stateCreator: StateCreator, _useStore: UseBoundStore> ) { expect(true).toBeTruthy() From 34d13b5178b3e3b47bfd78019583006b1073b2c2 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 11:38:43 +0530 Subject: [PATCH 062/144] migrate context.test.tsx --- tests/context.test.tsx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/context.test.tsx b/tests/context.test.tsx index c0fcf09364..c293b0544c 100644 --- a/tests/context.test.tsx +++ b/tests/context.test.tsx @@ -5,12 +5,9 @@ import { useState, } from 'react' import { render } from '@testing-library/react' -import create, { GetState, SetState } from 'zustand' +import create, { StoreApi } from 'zustand' import createContext from 'zustand/context' -import { - StoreApiWithSubscribeWithSelector, - subscribeWithSelector, -} from 'zustand/middleware' +import { subscribeWithSelector } from 'zustand/middleware' const consoleError = console.error afterEach(() => { @@ -23,7 +20,7 @@ type CounterState = { } it('creates and uses context store', async () => { - const { Provider, useStore } = createContext() + const { Provider, useStore } = createContext>() const createStore = () => create((set) => ({ @@ -47,7 +44,7 @@ it('creates and uses context store', async () => { }) it('uses context store with selectors', async () => { - const { Provider, useStore } = createContext() + const { Provider, useStore } = createContext>() const createStore = () => create((set) => ({ @@ -73,12 +70,7 @@ it('uses context store with selectors', async () => { it('uses context store api', async () => { const createStore = () => - create< - CounterState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector - >( + create()( subscribeWithSelector((set) => ({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), @@ -86,7 +78,7 @@ it('uses context store api', async () => { ) type CustomStore = ReturnType - const { Provider, useStoreApi } = createContext() + const { Provider, useStoreApi } = createContext() function Counter() { const storeApi = useStoreApi() @@ -139,7 +131,7 @@ it('throws error when not using provider', async () => { } } - const { useStore } = createContext() + const { useStore } = createContext>() function Component() { useStore() return
no error
From 92384437281c3d01498051775818ad92448ed2ee Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 11:39:00 +0530 Subject: [PATCH 063/144] fix devtools.test.tsx type erros --- tests/devtools.test.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/devtools.test.tsx b/tests/devtools.test.tsx index d6e5cb3ae8..5fdcd64b13 100644 --- a/tests/devtools.test.tsx +++ b/tests/devtools.test.tsx @@ -122,7 +122,7 @@ describe('when it receives an message of type...', () => { it('does nothing even if there is `api.dispatch`', () => { const initialState = { count: 0 } const api = create(devtools(() => initialState)) - api.dispatch = jest.fn() + ;(api as any).dispatch = jest.fn() const setState = jest.spyOn(api, 'setState') ;(extensionSubscriber as (message: any) => void)({ @@ -132,14 +132,14 @@ describe('when it receives an message of type...', () => { expect(api.getState()).toBe(initialState) expect(setState).not.toBeCalled() - expect(api.dispatch).not.toBeCalled() + expect((api as any).dispatch).not.toBeCalled() }) it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true', () => { const initialState = { count: 0 } const api = create(devtools(() => initialState)) - api.dispatch = jest.fn() - api.dispatchFromDevtools = true + ;(api as any).dispatch = jest.fn() + ;(api as any).dispatchFromDevtools = true const setState = jest.spyOn(api, 'setState') ;(extensionSubscriber as (message: any) => void)({ @@ -149,14 +149,16 @@ describe('when it receives an message of type...', () => { expect(api.getState()).toBe(initialState) expect(setState).not.toBeCalled() - expect(api.dispatch).toHaveBeenLastCalledWith({ type: 'INCREMENT' }) + expect((api as any).dispatch).toHaveBeenLastCalledWith({ + type: 'INCREMENT', + }) }) it('does not throw for unsupported payload', () => { const initialState = { count: 0 } const api = create(devtools(() => initialState)) - api.dispatch = jest.fn() - api.dispatchFromDevtools = true + ;(api as any).dispatch = jest.fn() + ;(api as any).dispatchFromDevtools = true const setState = jest.spyOn(api, 'setState') const originalConsoleError = console.error console.error = jest.fn() @@ -192,7 +194,7 @@ describe('when it receives an message of type...', () => { expect(api.getState()).toBe(initialState) expect(setState).not.toBeCalled() - expect(api.dispatch).not.toBeCalled() + expect((api as any).dispatch).not.toBeCalled() console.error = originalConsoleError }) From 1bf8afd6f47123a67893cedb7a19d9069eb0842c Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 11:46:50 +0530 Subject: [PATCH 064/144] context: remove callsignature in useStoreApi --- src/context.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/context.ts b/src/context.ts index ecf468d4e0..5d093efb27 100644 --- a/src/context.ts +++ b/src/context.ts @@ -21,6 +21,8 @@ export type UseContextStore = >( type ExtractState = S extends { getState: () => infer T } ? T : never +type WithoutCallSignature = { [K in keyof T]: T[K] } + function createContext>() { const ZustandContext = reactCreateContext(undefined) @@ -61,7 +63,7 @@ function createContext>() { ) } - const useStoreApi = (): S => { + const useStoreApi = () => { const store = useContext(ZustandContext) if (!store) { throw new Error( @@ -75,7 +77,7 @@ function createContext>() { setState: store.setState, subscribe: store.subscribe, destroy: store.destroy, - } as S), + } as WithoutCallSignature), [store] ) } From b95fb2666da4ee5da4765047d2d918d9473d82f2 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 11:52:10 +0530 Subject: [PATCH 065/144] context: remove `UseContextStore` type --- src/context.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/context.ts b/src/context.ts index 5d093efb27..ff0e25c292 100644 --- a/src/context.ts +++ b/src/context.ts @@ -14,11 +14,6 @@ import { useStore, } from 'zustand' -export type UseContextStore = >( - selector?: (state: ExtractState) => U, - equals?: (a: U, b: U) => boolean -) => U - type ExtractState = S extends { getState: () => infer T } ? T : never type WithoutCallSignature = { [K in keyof T]: T[K] } From cffa52a74c9e855da408bc28c8e93842e31cb410 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 11:55:18 +0530 Subject: [PATCH 066/144] context: fix useBoundStore type --- src/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/context.ts b/src/context.ts index ff0e25c292..ed2c37f837 100644 --- a/src/context.ts +++ b/src/context.ts @@ -41,7 +41,7 @@ function createContext>() { ) } - const useBoundStore: UseContextStore = ( + const useBoundStore = >( selector?: StateSelector, StateSlice>, equalityFn?: EqualityChecker ) => { From 5aa4bacde9c60e1164ea7576ffa7b3dd59fd1f7a Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 12:04:25 +0530 Subject: [PATCH 067/144] context: keep `UseContextStore` for tooltip just don't export it --- src/context.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/context.ts b/src/context.ts index ed2c37f837..682518c8b9 100644 --- a/src/context.ts +++ b/src/context.ts @@ -14,6 +14,11 @@ import { useStore, } from 'zustand' +type UseContextStore = >( + selector?: (state: ExtractState) => U, + equals?: (a: U, b: U) => boolean +) => U + type ExtractState = S extends { getState: () => infer T } ? T : never type WithoutCallSignature = { [K in keyof T]: T[K] } @@ -41,7 +46,7 @@ function createContext>() { ) } - const useBoundStore = >( + const useBoundStore: UseContextStore = >( selector?: StateSelector, StateSlice>, equalityFn?: EqualityChecker ) => { From 594c7fa90c96d3841bef61d65ebe94a51d5806ef Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 12:19:24 +0530 Subject: [PATCH 068/144] react: remove duplicate overload in create --- src/react.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/react.ts b/src/react.ts index 6a62b03192..724a30110b 100644 --- a/src/react.ts +++ b/src/react.ts @@ -43,8 +43,6 @@ type Create = { ( initializer: StateCreator ): UseBoundStore, Mos>> - >(store: S): UseBoundStore - (): ( initializer: StateCreator ) => UseBoundStore, Mos>> From d38a259dfedc284dd16b94393b12ac2c19f04213 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 12:56:29 +0530 Subject: [PATCH 069/144] export `WithPersist` --- src/middleware/persist.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index fc7f22e909..a87d3126fb 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -316,7 +316,7 @@ declare module '../vanilla' { } } -type WithPersist = S extends { getState: () => infer T } +export type WithPersist = S extends { getState: () => infer T } ? Write, A>> : never From 7a1ec6bec6232bedbec0b27f5d1399f2069331d1 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 12:58:52 +0530 Subject: [PATCH 070/144] devtools: preserve try/catch --- src/middleware/devtools.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index f9501a673b..abfe30b1b8 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -99,9 +99,14 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { return fn(set, get, api) } - const extensionConnector = - (window as any).__REDUX_DEVTOOLS_EXTENSION__ || - (window as any).top.__REDUX_DEVTOOLS_EXTENSION__ + let extensionConnector + try { + extensionConnector = + (window as any).__REDUX_DEVTOOLS_EXTENSION__ || + (window as any).top.__REDUX_DEVTOOLS_EXTENSION__ + } catch { + // ignored + } if (!extensionConnector) { if (process.env.NODE_ENV === 'development') { From 045ddb98d682aef1f38a1f14acc75ebda040d4b0 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 13:00:03 +0530 Subject: [PATCH 071/144] devtools: preserve window check --- src/middleware/devtools.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index abfe30b1b8..3bdbb8aa67 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -109,7 +109,10 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { } if (!extensionConnector) { - if (process.env.NODE_ENV === 'development') { + if ( + process.env.NODE_ENV === 'development' && + typeof window !== 'undefined' + ) { console.warn( '[zustand devtools middleware] Please install/enable Redux devtools extension' ) From 58d06bce4beeea515f62374d5784f9702f2f4ac4 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 13:08:08 +0530 Subject: [PATCH 072/144] add a test case for v3 style create --- tests/middlewareTypes.test.tsx | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/middlewareTypes.test.tsx b/tests/middlewareTypes.test.tsx index d9333113bf..b77f3a0923 100644 --- a/tests/middlewareTypes.test.tsx +++ b/tests/middlewareTypes.test.tsx @@ -557,3 +557,41 @@ describe('more complex state spec with subscribeWithSelector', () => { TestComponent }) }) + +describe('create with explicitly annotated mutators', () => { + it('subscribeWithSelector & persist', () => { + const useStore = create< + CounterState, + [ + ['zustand/subscribeWithSelector', never], + ['zustand/persist', Partial] + ] + >( + subscribeWithSelector( + persist( + (set, get) => ({ + count: 0, + inc: () => set({ count: get().count + 1 }, false), + }), + { name: 'count' } + ) + ) + ) + const TestComponent = () => { + useStore((s) => s.count) * 2 + useStore((s) => s.inc)() + useStore().count * 2 + useStore().inc() + useStore.getState().count * 2 + useStore.getState().inc() + useStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2) + ) + useStore.setState({ count: 0 }, false) + useStore.persist.hasHydrated() + return <> + } + TestComponent + }) +}) From 5c2ac1a3c2559c268425bdb96fa92c271e3c5641 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 13:42:36 +0530 Subject: [PATCH 073/144] devtools: preverse test fix from base branch --- src/middleware/devtools.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 3bdbb8aa67..6d15fa442e 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -147,6 +147,12 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { extension.subscribe((message: any) => { switch (message.type) { case 'ACTION': + if (typeof message.payload !== 'string') { + console.error( + '[zustand devtools middleware] Unsupported action format' + ) + return + } return parseJsonThen<{ type: unknown; state?: PartialState }>( message.payload, (action) => { From 14ded445db4ae314c377a3f3d0ba26cd6ad51b7e Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 9 Feb 2022 22:23:50 +0530 Subject: [PATCH 074/144] remove StoreApiWithFoo types, don't export WithFoo types --- src/middleware/devtools.ts | 8 ++----- src/middleware/immer.ts | 2 +- src/middleware/persist.ts | 7 +------ src/middleware/redux.ts | 12 +---------- src/middleware/subscribeWithSelector.ts | 28 +++++++++++++------------ 5 files changed, 20 insertions(+), 37 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 6d15fa442e..e6e21c2d50 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -55,10 +55,7 @@ interface DevtoolsOptions { } } -export type WithDevtools = Write< - Extract, - StoreSetStateWithAction -> +type WithDevtools = Write, StoreSetStateWithAction> type StoreSetStateWithAction = S extends { setState: (...a: infer A) => infer R @@ -80,11 +77,10 @@ type PopArgument unknown> = T extends ( : never type Write = Omit & U +type Cast = T extends U ? T : U export type NamedSet = WithDevtools>['setState'] -export type StoreApiWithDevtools = WithDevtools> - const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { type S = ReturnType diff --git a/src/middleware/immer.ts b/src/middleware/immer.ts index b85baa8a19..3ca7140b42 100644 --- a/src/middleware/immer.ts +++ b/src/middleware/immer.ts @@ -17,7 +17,7 @@ declare module 'zustand' { } } -export type WithImmer = S extends { +type WithImmer = S extends { getState: () => infer T setState: infer SetState } diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index a87d3126fb..8090246a44 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -87,11 +87,6 @@ type StorePersist = { } } -export type StoreApiWithPersist = WithPersist< - StoreApi, - Ps -> - type Thenable = { then( onFulfilled: (value: Value) => V | Promise | Thenable @@ -316,7 +311,7 @@ declare module '../vanilla' { } } -export type WithPersist = S extends { getState: () => infer T } +type WithPersist = S extends { getState: () => infer T } ? Write, A>> : never diff --git a/src/middleware/redux.ts b/src/middleware/redux.ts index 5898e4b727..8c48468c20 100644 --- a/src/middleware/redux.ts +++ b/src/middleware/redux.ts @@ -1,9 +1,4 @@ -import { - State, - StateCreator, - StoreApi, - StoreMutatorIdentifier, -} from '../vanilla' +import { State, StateCreator, StoreMutatorIdentifier } from '../vanilla' import { NamedSet } from './devtools' type Redux = < @@ -44,11 +39,6 @@ type PopArgument unknown> = T extends ( type Write = Omit & U -export type StoreApiWithRedux< - T extends State, - A extends { type: unknown } -> = WithRedux, A> - type ReduxImpl = ( reducer: (state: T, action: A) => T, initialState: T diff --git a/src/middleware/subscribeWithSelector.ts b/src/middleware/subscribeWithSelector.ts index 7d6351a498..d2df212021 100644 --- a/src/middleware/subscribeWithSelector.ts +++ b/src/middleware/subscribeWithSelector.ts @@ -2,7 +2,6 @@ import { State, StateCreator, StateListener, - StoreApi, StoreMutatorIdentifier, Subscribe, } from '../vanilla' @@ -27,23 +26,26 @@ declare module '../vanilla' { } type WithSelectorSubscribe = S extends { getState: () => infer T } - ? S & StoreSubscribeWithSelector> + ? Write>> : never +type Write = Omit & U +type Cast = T extends U ? T : U + interface StoreSubscribeWithSelector { - subscribe: ( - selector: (state: T) => U, - listener: (selectedState: U, previousSelectedState: U) => void, - options?: { - equalityFn?: (a: U, b: U) => boolean - fireImmediately?: boolean - } - ) => () => void + subscribe: { + (listener: (selectedState: T, previousSelectedState: T) => void): () => void + ( + selector: (state: T) => U, + listener: (selectedState: U, previousSelectedState: U) => void, + options?: { + equalityFn?: (a: U, b: U) => boolean + fireImmediately?: boolean + } + ): () => void + } } -export type StoreApiWithSubscribeWithSelector = - WithSelectorSubscribe> - type SubscribeWithSelectorImpl = ( storeInitializer: PopArgument> ) => PopArgument> From 0fac7b5a579b30c5079d334b6e806f51046c4b96 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Thu, 10 Feb 2022 19:13:32 +0530 Subject: [PATCH 075/144] style --- src/middleware/combine.ts | 4 ++-- src/middleware/devtools.ts | 6 +++--- src/middleware/persist.ts | 6 +++--- src/middleware/redux.ts | 7 ++++--- src/middleware/subscribeWithSelector.ts | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/middleware/combine.ts b/src/middleware/combine.ts index f82b06b787..8ca4ecbc0c 100644 --- a/src/middleware/combine.ts +++ b/src/middleware/combine.ts @@ -1,5 +1,7 @@ import { State, StateCreator, StoreMutatorIdentifier } from '../vanilla' +type Write = Omit & U + type Combine = < T extends State, U extends State, @@ -10,8 +12,6 @@ type Combine = < additionalStateCreator: StateCreator ) => StateCreator, Mps, Mcs> -type Write = Omit & U - export const combine: Combine = (initialState, create) => (...a) => diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index e6e21c2d50..9b7834e0a5 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -55,6 +55,9 @@ interface DevtoolsOptions { } } +type Write = Omit & U +type Cast = T extends U ? T : U + type WithDevtools = Write, StoreSetStateWithAction> type StoreSetStateWithAction = S extends { @@ -76,9 +79,6 @@ type PopArgument unknown> = T extends ( ? (...a: A) => R : never -type Write = Omit & U -type Cast = T extends U ? T : U - export type NamedSet = WithDevtools>['setState'] const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index 8090246a44..f1959773b6 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -311,6 +311,9 @@ declare module '../vanilla' { } } +type Write = Omit & U +type Cast = T extends U ? T : U + type WithPersist = S extends { getState: () => infer T } ? Write, A>> : never @@ -326,7 +329,4 @@ type PopArgument unknown> = T extends ( ? (...a: A) => R : never -type Write = Omit & U -type Cast = T extends U ? T : U - export const persist = persistImpl as unknown as Persist diff --git a/src/middleware/redux.ts b/src/middleware/redux.ts index 8c48468c20..49ad215d2e 100644 --- a/src/middleware/redux.ts +++ b/src/middleware/redux.ts @@ -16,7 +16,10 @@ declare module '../vanilla' { } } -type WithRedux = Write, StoreRedux>> +type Write = Omit & U +type Cast = T extends U ? T : U + +type WithRedux = Write, StoreRedux>> interface ReduxState
{ dispatch: StoreRedux['dispatch'] @@ -37,8 +40,6 @@ type PopArgument unknown> = T extends ( ? (...a: A) => R : never -type Write = Omit & U - type ReduxImpl = ( reducer: (state: T, action: A) => T, initialState: T diff --git a/src/middleware/subscribeWithSelector.ts b/src/middleware/subscribeWithSelector.ts index d2df212021..a1b68d4045 100644 --- a/src/middleware/subscribeWithSelector.ts +++ b/src/middleware/subscribeWithSelector.ts @@ -25,13 +25,13 @@ declare module '../vanilla' { } } +type Write = Omit & U +type Cast = T extends U ? T : U + type WithSelectorSubscribe = S extends { getState: () => infer T } ? Write>> : never -type Write = Omit & U -type Cast = T extends U ? T : U - interface StoreSubscribeWithSelector { subscribe: { (listener: (selectedState: T, previousSelectedState: T) => void): () => void From e2f4c744eb03538f3cfe9315f2f196d6b5881949 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Thu, 10 Feb 2022 19:15:12 +0530 Subject: [PATCH 076/144] devtools: preverse `originalIsRecording` change --- src/middleware/devtools.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 9b7834e0a5..810df3ab2a 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -132,9 +132,10 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { ) } const setStateFromDevtools: SetState = (...a) => { + const originalIsRecording = isRecording isRecording = false set(...a) - isRecording = true + isRecording = originalIsRecording } const initialState = fn(api.setState, get, api) From 970b0e26ae1d573d409dd877e829b4cbafdd3ab3 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 11 Feb 2022 18:06:18 +0900 Subject: [PATCH 077/144] fix bug in devtools --- src/middleware/devtools.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 810df3ab2a..88e89b15cf 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -207,9 +207,8 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { typeof (api as any).dispatch === 'function' ) { let didWarnAboutReservedActionType = false - const originalDispatch = ((api as any).dispatch(api as any).dispatch = ( - ...a: any[] - ) => { + const originalDispatch = (api as any).dispatch + ;(api as any).dispatch = (...a: any[]) => { if (a[0].type === '__setState' && !didWarnAboutReservedActionType) { console.warn( '[zustand devtools middleware] "__setState" action type is reserved ' + @@ -218,7 +217,7 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { didWarnAboutReservedActionType = true } ;(originalDispatch as any)(...a) - }) + } } return initialState From 8031d3f675a26294146be9b0a01a1e00e95f94a3 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 11 Feb 2022 21:05:44 +0900 Subject: [PATCH 078/144] empty commit From ac2dcaa7617389c6ebe8ab08c085e5b86ebaeb28 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 11 Feb 2022 21:08:05 +0900 Subject: [PATCH 079/144] 4.0.0-beta.1 --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f63de9a163..1647d0389a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "zustand", "private": true, - "version": "3.7.0", + "version": "4.0.0-beta.1", + "publishConfig": { + "tag": "next" + }, "description": "🐻 Bear necessities for state management in React", "main": "./index.js", "types": "./index.d.ts", From 01b42df916cf41846108d6236cb2b2ca76bfd58c Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 11 Feb 2022 21:10:45 +0900 Subject: [PATCH 080/144] fix lint --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1647d0389a..254900003f 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "private": true, "version": "4.0.0-beta.1", "publishConfig": { - "tag": "next" - }, + "tag": "next" + }, "description": "🐻 Bear necessities for state management in React", "main": "./index.js", "types": "./index.d.ts", From 4e162d0e6fa7a6bdfabba7032069fbb306d2b768 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 14 Feb 2022 00:01:15 +0530 Subject: [PATCH 081/144] style --- src/middleware/immer.ts | 4 ++-- src/middleware/redux.ts | 16 ++++++++-------- src/middleware/subscribeWithSelector.ts | 14 +++++++------- src/react.ts | 6 +++--- src/vanilla.ts | 12 ++++++------ 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/middleware/immer.ts b/src/middleware/immer.ts index 3ca7140b42..d6fb6d51f8 100644 --- a/src/middleware/immer.ts +++ b/src/middleware/immer.ts @@ -17,6 +17,8 @@ declare module 'zustand' { } } +type Write = Omit & U + type WithImmer = S extends { getState: () => infer T setState: infer SetState @@ -37,8 +39,6 @@ type WithImmer = S extends { > : never -type Write = Omit & U - type PopArgument unknown> = T extends ( ...a: [...infer A, infer _] ) => infer R diff --git a/src/middleware/redux.ts b/src/middleware/redux.ts index 49ad215d2e..90c33820c9 100644 --- a/src/middleware/redux.ts +++ b/src/middleware/redux.ts @@ -1,6 +1,11 @@ import { State, StateCreator, StoreMutatorIdentifier } from '../vanilla' import { NamedSet } from './devtools' +type Write = Omit & U +type Cast = T extends U ? T : U + +type WithRedux = Write, StoreRedux>> + type Redux = < T extends State, A extends Action, @@ -16,19 +21,14 @@ declare module '../vanilla' { } } -type Write = Omit & U -type Cast = T extends U ? T : U - -type WithRedux = Write, StoreRedux>> +interface Action { + type: unknown +} interface ReduxState { dispatch: StoreRedux['dispatch'] } -interface Action { - type: unknown -} - interface StoreRedux { dispatch: (a: A) => A dispatchFromDevtools: true diff --git a/src/middleware/subscribeWithSelector.ts b/src/middleware/subscribeWithSelector.ts index a1b68d4045..70ba70522e 100644 --- a/src/middleware/subscribeWithSelector.ts +++ b/src/middleware/subscribeWithSelector.ts @@ -18,13 +18,6 @@ type SubscribeWithSelector = < > ) => StateCreator -declare module '../vanilla' { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface StoreMutators { - ['zustand/subscribeWithSelector']: WithSelectorSubscribe - } -} - type Write = Omit & U type Cast = T extends U ? T : U @@ -32,6 +25,13 @@ type WithSelectorSubscribe = S extends { getState: () => infer T } ? Write>> : never +declare module '../vanilla' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface StoreMutators { + ['zustand/subscribeWithSelector']: WithSelectorSubscribe + } +} + interface StoreSubscribeWithSelector { subscribe: { (listener: (selectedState: T, previousSelectedState: T) => void): () => void diff --git a/src/react.ts b/src/react.ts index 724a30110b..6c58794439 100644 --- a/src/react.ts +++ b/src/react.ts @@ -49,7 +49,7 @@ type Create = { >(store: S): UseBoundStore } -const _create = (createState: StateCreator) => { +const createImpl = (createState: StateCreator) => { const api = typeof createState === 'function' ? createStore(createState) : createState @@ -64,8 +64,8 @@ const _create = (createState: StateCreator) => { const create = (( createState: StateCreator | undefined ) => { - if (!createState) return _create - return _create(createState) + if (!createState) return createImpl + return createImpl(createState) }) as Create export default create diff --git a/src/vanilla.ts b/src/vanilla.ts index 74d702d020..04e7e550ab 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -67,6 +67,12 @@ type CreateStoreImpl = < initializer: StateCreator ) => Mutate, Mos> +type PopArgument unknown> = T extends ( + ...a: [...infer A, infer _] +) => infer R + ? (...a: A) => R + : never + const createStoreImpl: CreateStoreImpl = (createState) => { type TState = ReturnType let state: TState @@ -106,12 +112,6 @@ const createStoreImpl: CreateStoreImpl = (createState) => { return api as any } -type PopArgument unknown> = T extends ( - ...a: [...infer A, infer _] -) => infer R - ? (...a: A) => R - : never - const createStore = ((f) => { if (f === undefined) return createStoreImpl return createStoreImpl(f) From 3d6d26f0f678a0c8a9c11750d4ccee974591b0b7 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 14 Feb 2022 00:20:50 +0530 Subject: [PATCH 082/144] export immer fix tests --- src/middleware.ts | 1 + tests/middlewareTypes.test.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/middleware.ts b/src/middleware.ts index 90642820a8..c1787bac3d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,3 +3,4 @@ export * from './middleware/devtools' export * from './middleware/subscribeWithSelector' export * from './middleware/combine' export * from './middleware/persist' +export * from './middleware/immer' diff --git a/tests/middlewareTypes.test.tsx b/tests/middlewareTypes.test.tsx index 087d923dc8..69a1ae28cd 100644 --- a/tests/middlewareTypes.test.tsx +++ b/tests/middlewareTypes.test.tsx @@ -2,11 +2,11 @@ import create from 'zustand' import { combine, devtools, + immer, persist, redux, subscribeWithSelector, } from 'zustand/middleware' -import { immer } from 'zustand/middleware/immer' type CounterState = { count: number From 36a92f7d2bf017b27a465231e404101f15182188 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Thu, 17 Feb 2022 16:12:10 +0530 Subject: [PATCH 083/144] style, minor fixes --- src/middleware/devtools.ts | 10 +++++----- src/middleware/redux.ts | 26 +++++++++++++------------- src/react.ts | 5 +---- src/vanilla.ts | 6 ++---- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 8a62d4c5fe..19d0b8be37 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -118,10 +118,6 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { ? { name: options } : options - if (typeof window === 'undefined') { - return fn(set, get, api) - } - let extensionConnector try { extensionConnector = @@ -233,7 +229,11 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { let didWarnAboutReservedActionType = false const originalDispatch = (api as any).dispatch ;(api as any).dispatch = (...a: any[]) => { - if (a[0].type === '__setState' && !didWarnAboutReservedActionType) { + if ( + __DEV__ && + a[0].type === '__setState' && + !didWarnAboutReservedActionType + ) { console.warn( '[zustand devtools middleware] "__setState" action type is reserved ' + 'to set state from the devtools. Avoid using it.' diff --git a/src/middleware/redux.ts b/src/middleware/redux.ts index 90c33820c9..8bffda989e 100644 --- a/src/middleware/redux.ts +++ b/src/middleware/redux.ts @@ -4,6 +4,19 @@ import { NamedSet } from './devtools' type Write = Omit & U type Cast = T extends U ? T : U +interface Action { + type: unknown +} + +interface ReduxState { + dispatch: StoreRedux['dispatch'] +} + +interface StoreRedux { + dispatch: (a: A) => A + dispatchFromDevtools: true +} + type WithRedux = Write, StoreRedux>> type Redux = < @@ -21,19 +34,6 @@ declare module '../vanilla' { } } -interface Action { - type: unknown -} - -interface ReduxState { - dispatch: StoreRedux['dispatch'] -} - -interface StoreRedux { - dispatch: (a: A) => A - dispatchFromDevtools: true -} - type PopArgument unknown> = T extends ( ...a: [...infer A, infer _] ) => infer R diff --git a/src/react.ts b/src/react.ts index 6c58794439..ee64992b8d 100644 --- a/src/react.ts +++ b/src/react.ts @@ -63,9 +63,6 @@ const createImpl = (createState: StateCreator) => { const create = (( createState: StateCreator | undefined -) => { - if (!createState) return createImpl - return createImpl(createState) -}) as Create +) => (createState ? createImpl(createState) : createImpl)) as Create export default create diff --git a/src/vanilla.ts b/src/vanilla.ts index f0c5b0953f..fd538de94e 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -115,9 +115,7 @@ const createStoreImpl: CreateStoreImpl = (createState) => { return api as any } -const createStore = ((f) => { - if (f === undefined) return createStoreImpl - return createStoreImpl(f) -}) as CreateStore +const createStore = ((createState) => + createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore export default createStore From 82cc93a34d6c05d200be3c3da29566f01907efb2 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Thu, 17 Feb 2022 16:22:13 +0530 Subject: [PATCH 084/144] devtools: fix test --- tests/devtools.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/devtools.test.tsx b/tests/devtools.test.tsx index e7dc809d32..4f9cc82327 100644 --- a/tests/devtools.test.tsx +++ b/tests/devtools.test.tsx @@ -439,6 +439,8 @@ describe('when it receives an message of type...', () => { }) it('works with redux middleware', () => { + const savedDEV = __DEV__ + __DEV__ = true const api = create( devtools( redux( @@ -475,6 +477,7 @@ it('works with redux middleware', () => { ) console.warn = originalConsoleWarn + __DEV__ = savedDEV }) it('works in non-browser env', () => { From 3687f6893f85cb21b15fb55debcb82371718001f Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Fri, 18 Feb 2022 09:07:12 +0900 Subject: [PATCH 085/144] Update tests/devtools.test.tsx --- tests/devtools.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devtools.test.tsx b/tests/devtools.test.tsx index 4f9cc82327..e3d062fdd8 100644 --- a/tests/devtools.test.tsx +++ b/tests/devtools.test.tsx @@ -438,7 +438,7 @@ describe('when it receives an message of type...', () => { }) }) -it('works with redux middleware', () => { +it('[DEV-ONLY] works with redux middleware', () => { const savedDEV = __DEV__ __DEV__ = true const api = create( From be51c334a454a67beff6fe3afa3426c3f917a5cf Mon Sep 17 00:00:00 2001 From: daishi Date: Tue, 22 Feb 2022 09:32:51 +0900 Subject: [PATCH 086/144] breaking(middleware/devtools): use official devtools extension types --- package.json | 2 ++ src/middleware/devtools.ts | 36 +++++++++++++++++------------------- yarn.lock | 16 +++++++++++++++- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index b6adb6cc6b..73487bedfe 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "@babel/plugin-transform-runtime": "^7.17.0", "@babel/plugin-transform-typescript": "^7.16.8", "@babel/preset-env": "^7.16.11", + "@redux-devtools/extension": "^3.2.2", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-replace": "^3.0.1", @@ -167,6 +168,7 @@ "prettier": "^2.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "redux": "^5.0.0-alpha.0", "rollup": "^2.67.2", "rollup-plugin-esbuild": "^4.8.2", "rollup-plugin-terser": "^7.0.2", diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index f026118086..1d5db197ed 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -1,3 +1,5 @@ +import '@redux-devtools/extension' + import { GetState, PartialState, SetState, State, StoreApi } from '../vanilla' declare module '../vanilla' { @@ -27,21 +29,19 @@ type StoreSetStateWithAction = S extends { getState: () => infer T } interface DevtoolsOptions { name?: string anonymousActionType?: string - serialize?: { - options: - | boolean - | { - date?: boolean - regex?: boolean - undefined?: boolean - nan?: boolean - infinity?: boolean - error?: boolean - symbol?: boolean - map?: boolean - set?: boolean - } - } + serialize?: + | boolean + | { + date?: boolean + regex?: boolean + undefined?: boolean + nan?: boolean + infinity?: boolean + error?: boolean + symbol?: boolean + map?: boolean + set?: boolean + } } type DevtoolsType = { @@ -170,16 +170,14 @@ export function devtools< } const devtoolsOptions = options === undefined - ? { name: undefined, anonymousActionType: undefined } + ? {} : typeof options === 'string' ? { name: options } : options let extensionConnector try { - extensionConnector = - (window as any).__REDUX_DEVTOOLS_EXTENSION__ || - (window as any).top.__REDUX_DEVTOOLS_EXTENSION__ + extensionConnector = window.__REDUX_DEVTOOLS_EXTENSION__ } catch { // ignored } diff --git a/yarn.lock b/yarn.lock index f53c07154f..4cf394ca40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -922,7 +922,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.17.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -1227,6 +1227,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@redux-devtools/extension@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@redux-devtools/extension/-/extension-3.2.2.tgz#2d6da4df2c4d32a0aac54d824e46f52b1fd9fc4d" + integrity sha512-fKA2TWNzJF7wXSDwBemwcagBFudaejXCzH5hRszN3Z6B7XEJtEmGD77AjV0wliZpIZjA/fs3U7CejFMQ+ipS7A== + dependencies: + "@babel/runtime" "^7.17.0" + "@rollup/plugin-babel@^5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" @@ -4378,6 +4385,13 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +redux@^5.0.0-alpha.0: + version "5.0.0-alpha.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.0-alpha.0.tgz#a787df7b92a69af70900c84586fc2dc89ca97ab5" + integrity sha512-9NQVVttmTiwECalBRd6sKWW4e8u6ekR1rlfsHy0ZOU95kcDpKp4enL8xbNZkKgT7GhnNmlfNZp1HWlHs+XZaww== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" From 06123b102a9c69a859f8a69204c7a11ad4978a0b Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 24 Feb 2022 08:41:22 +0900 Subject: [PATCH 087/144] type object.create --- src/middleware/devtools.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 1d5db197ed..8c29f8e48a 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -191,7 +191,9 @@ export function devtools< return fn(set, get, api) } - let extension = Object.create(extensionConnector.connect(devtoolsOptions)) + let extension = (Object.create as (t: T) => T)( + extensionConnector.connect(devtoolsOptions) + ) // We're using `Object.defineProperty` to set `prefix`, so if extensionConnector.connect // returns the same reference we'd get cannot redefine property prefix error // hence we `Object.create` to make a new reference From e9cf15e5745bca2faae2190a07f49dc78f27037a Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 24 Feb 2022 08:48:53 +0900 Subject: [PATCH 088/144] avoid emitting @redux-devtools/extension --- src/middleware/devtools.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 8c29f8e48a..ad4c26eecd 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -1,5 +1,4 @@ -import '@redux-devtools/extension' - +import type {} from '@redux-devtools/extension' import { GetState, PartialState, SetState, State, StoreApi } from '../vanilla' declare module '../vanilla' { From 4a30a95f51ea19df8b2deb99cf98b7872436bd7f Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 24 Feb 2022 09:17:49 +0900 Subject: [PATCH 089/144] fix type with any --- src/middleware/devtools.ts | 108 +++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index ad4c26eecd..b5fe9dfaf5 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -294,66 +294,70 @@ export function devtools< } } - extension.subscribe((message: any) => { - switch (message.type) { - case 'ACTION': - if (typeof message.payload !== 'string') { - console.error( - '[zustand devtools middleware] Unsupported action format' - ) - return - } - return parseJsonThen<{ type: unknown; state?: PartialState }>( - message.payload, - (action) => { - if (action.type === '__setState') { - setStateFromDevtools(action.state as PartialState) - return + ;(extension as any) // FIXME no-any + .subscribe((message: any) => { + switch (message.type) { + case 'ACTION': + if (typeof message.payload !== 'string') { + console.error( + '[zustand devtools middleware] Unsupported action format' + ) + return + } + return parseJsonThen<{ type: unknown; state?: PartialState }>( + message.payload, + (action) => { + if (action.type === '__setState') { + setStateFromDevtools(action.state as PartialState) + return + } + + if (!api.dispatchFromDevtools) return + if (typeof api.dispatch !== 'function') return + ;(api.dispatch as any)(action) } + ) - if (!api.dispatchFromDevtools) return - if (typeof api.dispatch !== 'function') return - ;(api.dispatch as any)(action) - } - ) + case 'DISPATCH': + switch (message.payload.type) { + case 'RESET': + setStateFromDevtools(initialState) + return extension.init(api.getState()) - case 'DISPATCH': - switch (message.payload.type) { - case 'RESET': - setStateFromDevtools(initialState) - return extension.init(api.getState()) + case 'COMMIT': + return extension.init(api.getState()) - case 'COMMIT': - return extension.init(api.getState()) + case 'ROLLBACK': + return parseJsonThen(message.state, (state) => { + setStateFromDevtools(state) + extension.init(api.getState()) + }) - case 'ROLLBACK': - return parseJsonThen(message.state, (state) => { - setStateFromDevtools(state) - extension.init(api.getState()) - }) + case 'JUMP_TO_STATE': + case 'JUMP_TO_ACTION': + return parseJsonThen(message.state, (state) => { + setStateFromDevtools(state) + }) - case 'JUMP_TO_STATE': - case 'JUMP_TO_ACTION': - return parseJsonThen(message.state, (state) => { - setStateFromDevtools(state) - }) + case 'IMPORT_STATE': { + const { nextLiftedState } = message.payload + const lastComputedState = + nextLiftedState.computedStates.slice(-1)[0]?.state + if (!lastComputedState) return + setStateFromDevtools(lastComputedState) + extension.send( + null as any, // FIXME no-any + nextLiftedState + ) + return + } - case 'IMPORT_STATE': { - const { nextLiftedState } = message.payload - const lastComputedState = - nextLiftedState.computedStates.slice(-1)[0]?.state - if (!lastComputedState) return - setStateFromDevtools(lastComputedState) - extension.send(null, nextLiftedState) - return + case 'PAUSE_RECORDING': + return (isRecording = !isRecording) } - - case 'PAUSE_RECORDING': - return (isRecording = !isRecording) - } - return - } - }) + return + } + }) return initialState } From 86d4f977cc500a9c279f63e2bc605dd8f4262e18 Mon Sep 17 00:00:00 2001 From: daishi Date: Thu, 24 Feb 2022 17:43:15 +0900 Subject: [PATCH 090/144] refactor --- src/middleware/devtools.ts | 117 +++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index b5fe9dfaf5..5ee374c807 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -294,70 +294,75 @@ export function devtools< } } - ;(extension as any) // FIXME no-any - .subscribe((message: any) => { - switch (message.type) { - case 'ACTION': - if (typeof message.payload !== 'string') { - console.error( - '[zustand devtools middleware] Unsupported action format' - ) - return - } - return parseJsonThen<{ type: unknown; state?: PartialState }>( - message.payload, - (action) => { - if (action.type === '__setState') { - setStateFromDevtools(action.state as PartialState) - return - } - - if (!api.dispatchFromDevtools) return - if (typeof api.dispatch !== 'function') return - ;(api.dispatch as any)(action) - } + ;( + extension as unknown as { + subscribe: ( + listener: (message: any) => void // FIXME no-any + ) => (() => void) | undefined + } + ).subscribe((message) => { + switch (message.type) { + case 'ACTION': + if (typeof message.payload !== 'string') { + console.error( + '[zustand devtools middleware] Unsupported action format' ) + return + } + return parseJsonThen<{ type: unknown; state?: PartialState }>( + message.payload, + (action) => { + if (action.type === '__setState') { + setStateFromDevtools(action.state as PartialState) + return + } - case 'DISPATCH': - switch (message.payload.type) { - case 'RESET': - setStateFromDevtools(initialState) - return extension.init(api.getState()) + if (!api.dispatchFromDevtools) return + if (typeof api.dispatch !== 'function') return + ;(api.dispatch as any)(action) + } + ) - case 'COMMIT': - return extension.init(api.getState()) + case 'DISPATCH': + switch (message.payload.type) { + case 'RESET': + setStateFromDevtools(initialState) + return extension.init(api.getState()) - case 'ROLLBACK': - return parseJsonThen(message.state, (state) => { - setStateFromDevtools(state) - extension.init(api.getState()) - }) + case 'COMMIT': + return extension.init(api.getState()) - case 'JUMP_TO_STATE': - case 'JUMP_TO_ACTION': - return parseJsonThen(message.state, (state) => { - setStateFromDevtools(state) - }) + case 'ROLLBACK': + return parseJsonThen(message.state, (state) => { + setStateFromDevtools(state) + extension.init(api.getState()) + }) - case 'IMPORT_STATE': { - const { nextLiftedState } = message.payload - const lastComputedState = - nextLiftedState.computedStates.slice(-1)[0]?.state - if (!lastComputedState) return - setStateFromDevtools(lastComputedState) - extension.send( - null as any, // FIXME no-any - nextLiftedState - ) - return - } + case 'JUMP_TO_STATE': + case 'JUMP_TO_ACTION': + return parseJsonThen(message.state, (state) => { + setStateFromDevtools(state) + }) - case 'PAUSE_RECORDING': - return (isRecording = !isRecording) + case 'IMPORT_STATE': { + const { nextLiftedState } = message.payload + const lastComputedState = + nextLiftedState.computedStates.slice(-1)[0]?.state + if (!lastComputedState) return + setStateFromDevtools(lastComputedState) + extension.send( + null as any, // FIXME no-any + nextLiftedState + ) + return } - return - } - }) + + case 'PAUSE_RECORDING': + return (isRecording = !isRecording) + } + return + } + }) return initialState } From 92a70945336fbcf04b043081203a135e9f724c2d Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 28 Feb 2022 12:03:09 +0900 Subject: [PATCH 091/144] fix yarn lock --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8473f1c274..bb6829b886 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5073,10 +5073,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@1.0.0-rc.0: - version "1.0.0-rc.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-rc.0.tgz#0d8fb7cbc31ddfb3ee01225f6b0a700cf59c449b" - integrity sha512-0U9Xlc2QDFzSGMB0DvcJQL0+DIdxDPJC7mnZlYFbl7wrSrPMcs89X5TVkNB6Dzg618m8lZop+U+J6ow3vq9RAQ== +use-sync-external-store@1.0.0-rc.1-next-629036a9c-20220224: + version "1.0.0-rc.1-next-629036a9c-20220224" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.0.0-rc.1-next-629036a9c-20220224.tgz#40cf472454789403c2de6c8471d177459d184dc1" + integrity sha512-IhuMl0apVVYsT3XPfV+0nuwf0T6+3d4YxQXV4tDRsGpSQcYVG4zoWwfX4zdtouUfuelYg4t2SEmFifIMrxPfIw== v8-compile-cache@^2.0.3: version "2.3.0" From 64536ee4332193c7cafd4c5f47841d28051ff004 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 28 Feb 2022 12:08:47 +0900 Subject: [PATCH 092/144] temporary fix #829 --- rollup.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rollup.config.js b/rollup.config.js index d480fa7cf1..adbbb1fb5e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -60,6 +60,9 @@ function createESMConfig(input, output) { resolve({ extensions }), replace({ __DEV__: '(import.meta.env&&import.meta.env.MODE)!=="production"', + // a workround for #829 + 'use-sync-external-store/shim/with-selector': + 'use-sync-external-store/shim/with-selector.js', preventAssignment: true, }), getEsbuild('node12'), From 0c0f3a35d89d732ff5a47e107b86d3baf34742c8 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 28 Feb 2022 12:15:35 +0900 Subject: [PATCH 093/144] v4.0.0-beta.2 --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fe89e66b50..b4090d4837 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "zustand", "private": true, - "version": "3.7.1", + "version": "4.0.0-beta.2", + "publishConfig": { + "tag": "next" + }, "description": "🐻 Bear necessities for state management in React", "main": "./index.js", "types": "./index.d.ts", From 304ee89cfe86d7a0fab4299cbae91ceb7bb297b1 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 28 Feb 2022 12:18:22 +0900 Subject: [PATCH 094/144] fix lint --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b4090d4837..2d9dadf9db 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "4.0.0-beta.2", "publishConfig": { - "tag": "next" + "tag": "next" }, "description": "🐻 Bear necessities for state management in React", "main": "./index.js", From d4b72002b9d1368975b294fc81835cc41c6e8851 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 4 Mar 2022 11:30:46 +0900 Subject: [PATCH 095/144] lock date-fns version --- package.json | 3 +++ yarn.lock | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 2d9dadf9db..6ec9646e8d 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,9 @@ "shx": "^0.3.4", "typescript": "^4.5.5" }, + "resolutions": { + "date-fns": "2.27.0" + }, "peerDependencies": { "react": ">=16.8" }, diff --git a/yarn.lock b/yarn.lock index d4e1ca4cfe..26ad9ae863 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2192,10 +2192,10 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -date-fns@^2.16.1: - version "2.28.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" - integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw== +date-fns@2.27.0, date-fns@^2.16.1: + version "2.27.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.27.0.tgz#e1ff3c3ddbbab8a2eaadbb6106be2929a5a2d92b" + integrity sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q== debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: version "4.3.3" From 48b0267f97619cae90c2f74a1e0135350a084556 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 5 Mar 2022 19:57:03 +0530 Subject: [PATCH 096/144] test middleware subtyping --- tests/middlewareTypes.test.tsx | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/middlewareTypes.test.tsx b/tests/middlewareTypes.test.tsx index 69a1ae28cd..e6a18c6b55 100644 --- a/tests/middlewareTypes.test.tsx +++ b/tests/middlewareTypes.test.tsx @@ -1,4 +1,4 @@ -import create from 'zustand' +import create, { StoreApi } from 'zustand' import { combine, devtools, @@ -7,6 +7,7 @@ import { redux, subscribeWithSelector, } from 'zustand/middleware' +import createVanilla from 'zustand/vanilla' type CounterState = { count: number @@ -61,6 +62,10 @@ describe('counter state spec (single middleware)', () => { return <> } TestComponent + + const _testSubtyping: StoreApi<{ count: number }> = createVanilla( + immer(() => ({ count: 0 })) + ) }) it('redux', () => { @@ -85,6 +90,10 @@ describe('counter state spec (single middleware)', () => { return <> } TestComponent + + const _testSubtyping: StoreApi<{ count: number }> = createVanilla( + redux((x) => x, { count: 0 }) + ) }) it('devtools', () => { @@ -109,6 +118,10 @@ describe('counter state spec (single middleware)', () => { return <> } TestComponent + + const _testSubtyping: StoreApi<{ count: number }> = createVanilla( + devtools(() => ({ count: 0 })) + ) }) it('subscribeWithSelector', () => { @@ -132,6 +145,10 @@ describe('counter state spec (single middleware)', () => { return <> } TestComponent + + const _testSubtyping: StoreApi<{ count: number }> = createVanilla( + subscribeWithSelector(() => ({ count: 0 })) + ) }) it('combine', () => { @@ -150,6 +167,10 @@ describe('counter state spec (single middleware)', () => { return <> } TestComponent + + const _testSubtyping: StoreApi<{ count: number }> = createVanilla( + combine({ count: 0 }, () => ({})) + ) }) it('persist', () => { @@ -173,6 +194,10 @@ describe('counter state spec (single middleware)', () => { return <> } TestComponent + + const _testSubtyping: StoreApi<{ count: number }> = createVanilla( + persist(() => ({ count: 0 })) + ) }) it('persist without custom api (#638)', () => { From b653b4b62bdd05efe05d751be6dd0f39d70ec50e Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sat, 5 Mar 2022 23:52:10 +0530 Subject: [PATCH 097/144] fix errors in conflict resolution --- src/context.ts | 9 ++++++--- tests/context.test.tsx | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/context.ts b/src/context.ts index 5fc2dbcf35..4568bd00fb 100644 --- a/src/context.ts +++ b/src/context.ts @@ -8,9 +8,12 @@ import { } from 'react' import { EqualityChecker, State, StateSelector, StoreApi, useStore } from '.' -type UseContextStore = { - (): T - (selector: StateSelector, equalityFn?: EqualityChecker): U +type UseContextStore> = { + (): ExtractState + ( + selector: StateSelector, U>, + equalityFn?: EqualityChecker + ): U } type ExtractState = S extends { getState: () => infer T } ? T : never diff --git a/tests/context.test.tsx b/tests/context.test.tsx index c52137d7c3..ff2f0afbaa 100644 --- a/tests/context.test.tsx +++ b/tests/context.test.tsx @@ -147,7 +147,7 @@ it('throws error when not using provider', async () => { }) it('useCallback with useStore infers types correctly', async () => { - const { useStore } = createContext() + const { useStore } = createContext>() function _Counter() { const _x = useStore(useCallback((state) => state.count, [])) expectAreTypesEqual().toBe(true) From 129b64b8da65fc83bf75997f84f85a994b44e1f2 Mon Sep 17 00:00:00 2001 From: daishi Date: Sat, 5 Mar 2022 10:05:28 +0900 Subject: [PATCH 098/144] lock testing-library/react alpha version --- .github/workflows/test-multiple-versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-multiple-versions.yml b/.github/workflows/test-multiple-versions.yml index fa625aa136..292e111e53 100644 --- a/.github/workflows/test-multiple-versions.yml +++ b/.github/workflows/test-multiple-versions.yml @@ -46,7 +46,7 @@ jobs: - run: yarn install --frozen-lockfile --check-files - name: Install alpha testing-library if: ${{ matrix.testing == 'alpha' }} - run: yarn add -D @testing-library/react@alpha + run: yarn add -D @testing-library/react@13.0.0-alpha.5 - name: Patch for React 16 if: ${{ startsWith(matrix.react, '16.') }} run: | From 7f54b3c1013202d2a51dc4032b98d5cebae0a51f Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 20 Mar 2022 17:22:46 +0530 Subject: [PATCH 099/144] more correct (and strict) persist types --- src/middleware/persist.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index f1959773b6..1fbe6e7e8a 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -5,19 +5,15 @@ import { StoreMutatorIdentifier, } from '../vanilla' -type DeepPartial = { - [P in keyof T]?: DeepPartial -} - export type StateStorage = { getItem: (name: string) => string | null | Promise setItem: (name: string, value: string) => void | Promise removeItem: (name: string) => void | Promise } -type StorageValue = { state: DeepPartial; version?: number } +type StorageValue = { state: S; version?: number } -export type PersistOptions> = { +export type PersistOptions = { /** Name of the storage (must be unique) */ name: string /** @@ -50,13 +46,15 @@ export type PersistOptions> = { * * @params state The state's value */ - partialize?: (state: S) => DeepPartial + partialize?: (state: S) => PersistedState /** * A function returning another (optional) function. * The main function will be called before the state rehydration. * The returned function will be called after the state rehydration or when an error occurred. */ - onRehydrateStorage?: (state: S) => ((state?: S, error?: Error) => void) | void + onRehydrateStorage?: ( + state: S + ) => ((state?: S, error?: unknown) => void) | void /** * If the stored state's version mismatch the one specified here, the storage will not be used. * This is useful when adding a breaking change to your store. @@ -66,12 +64,12 @@ export type PersistOptions> = { * A function to perform persisted state migration. * This function will be called when persisted state versions mismatch with the one specified here. */ - migrate?: (persistedState: any, version: number) => S | Promise + migrate?: (persistedState: unknown, version: number) => S | Promise /** * A function to perform custom hydration merges when combining the stored state with the current one. * By default, this function does a shallow merge. */ - merge?: (persistedState: any, currentState: S) => S + merge?: (persistedState: unknown, currentState: S) => S } type PersistListener = (state: S) => void @@ -131,12 +129,12 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { let options = { getStorage: () => localStorage, serialize: JSON.stringify as (state: StorageValue) => string, - deserialize: JSON.parse as (str: string) => StorageValue>, + deserialize: JSON.parse as (str: string) => StorageValue, partialize: (state: S) => state, version: 0, - merge: (persistedState: any, currentState: S) => ({ + merge: (persistedState: unknown, currentState: S) => ({ ...currentState, - ...persistedState, + ...(typeof persistedState === 'object' ? persistedState : {}), }), ...baseOptions, } @@ -258,7 +256,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { }) } - ;(api as StoreApi & StorePersist).persist = { + ;(api as StoreApi & StorePersist).persist = { setOptions: (newOptions) => { options = { ...options, @@ -320,7 +318,7 @@ type WithPersist = S extends { getState: () => infer T } type PersistImpl = ( storeInitializer: PopArgument>, - options: PersistOptions + options: PersistOptions ) => PopArgument> type PopArgument unknown> = T extends ( From 414c398f3e39689c8353c59892f05330cf6ef709 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 20 Mar 2022 17:39:19 +0530 Subject: [PATCH 100/144] migrate tests --- tests/persistAsync.test.tsx | 3 ++- tests/persistSync.test.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/persistAsync.test.tsx b/tests/persistAsync.test.tsx index 592dfe7345..6f1bd97033 100644 --- a/tests/persistAsync.test.tsx +++ b/tests/persistAsync.test.tsx @@ -354,7 +354,8 @@ describe('persist middleware with async configuration', () => { persist(() => ({ count: 0, actions: { unstorableMethod } }), { name: 'test-storage', getStorage: () => storage, - merge: (persistedState, currentState) => { + merge: (_persistedState, currentState) => { + const persistedState = _persistedState as any delete persistedState.actions return { diff --git a/tests/persistSync.test.tsx b/tests/persistSync.test.tsx index 6ef1bb42fc..798c98745e 100644 --- a/tests/persistSync.test.tsx +++ b/tests/persistSync.test.tsx @@ -271,7 +271,8 @@ describe('persist middleware with sync configuration', () => { persist(() => ({ count: 0, actions: { unstorableMethod } }), { name: 'test-storage', getStorage: () => storage, - merge: (persistedState, currentState) => { + merge: (_persistedState, currentState) => { + const persistedState = _persistedState as any delete persistedState.actions return { From 155a177f4f39f139c2f8148c46dd575e919277cb Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 21 Mar 2022 22:34:31 +0530 Subject: [PATCH 101/144] wip release notes --- v4.0.0-rc-release-notes.md | 142 +++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 v4.0.0-rc-release-notes.md diff --git a/v4.0.0-rc-release-notes.md b/v4.0.0-rc-release-notes.md new file mode 100644 index 0000000000..07886c1c4f --- /dev/null +++ b/v4.0.0-rc-release-notes.md @@ -0,0 +1,142 @@ +# v4.0.0 + +## At a glance + +### TypeScript Improvements + +Imagine you want to have a store that uses `devtools` and `persist` middleware. In v3.7.x you'd write this something like... + +```typescript +import create, { GetState, SetState } from "zustand" +import { devtools, persist, StoreApiWithDevtools, StoreApiWithPersist } from "zustand/middleware" + +interface BearState { + count: number, + reset: () => void, + clearPersistStorage: () => void +} + +let useStore = create< + BearState, + SetState, + GetState, + StoreApiWithDevtools & StoreApiWithPersist + // or in v3.7.x as... + // Mutate, [["zustand/persist", BearState], ["zustand/devtools", never]]> +>( + persist( + devtools( + (set, get, store) => + ({ + count: 0, + reset: () => set({ count: 0 }, false, "reset"), + clearPersistStorage: () => store.persist.clearStorage() + }) + ), + { name: "temp" } + ) +) +let bearState = useStore() +// ^? +``` + +But now all you have to annotate is your state... + +```typescript +import create, { GetState, SetState } from "zustand" +import { devtools, persist, StoreApiWithDevtools, StoreApiWithPersist } from "zustand/middleware" + +interface BearState { + count: number, + reset: () => void, + clearPersistStorage: () => void +} + +let useStore = create()( + persist( + devtools( + (set, get, store) => + ({ + count: 0, + reset: () => set({ count: 0 }, false, "reset"), + clearPersistStorage: () => store.persist.clearStorage() + }) + ), + { name: "temp" } + ) +) +let bearState = useStore() +// ^? +``` + +Middlewares in zustand can and do mutate the store, which makes it almost impossible to type them correctly and make the inference work. But nonetheless @devanshj pulled it off in [#725](https://github.com/pmndrs/zustand/pull/725), if you're curious how it works you can read [#710](https://github.com/pmndrs/zustand/issues/710). + +### React 18 + +something something `useSyncExternalStore` something something + +### Some other feature + +whatever + +## Breaking changes and migration + +If you're not using the typed version (either via TypeScript or via JSDoc) then there are no breaking changes for you and hence no migration is needed either. + +### `create` (from `zustand`, `zustand/vanilla`, or `zustand/react`) + +#### Change + +```diff + // Pseudo diff +- create: +- < TState +- , TStoreSetState = StoreApi["set"] +- , TStoreGetState = StoreApi["get"] +- , TStore = StoreApi +- > +- (f: ...) => ... ++ create: ++ { (): (f: ...) => ... ++ , (f: ...) => ... ++ } +``` + +#### Migration + +If you're passing zero generics to `create` then there is no migration needed. Else do one of these two things... + +- (Recommended) Write `create()(...)` instead of `create(...)` + We use currying (that doesn't do anything in the runtime) as a workaround for [microsoft/TypeScript#10571](https://github.com/microsoft/TypeScript/issues/10571) +- Write `create(...)` instead of `create()`. + +### `StateCreator` + +#### Change + +```diff + // Pseudo diff +- type StateCreator +- < TState +- , TStoreSetState = StoreApi["set"] +- , TStoreGetState = StoreApi["get"] +- , TStore = StoreApi +- > = +- ... ++ type StateCreator ++ < TState ++ , TInMutators extends [StoreMutatorIdentifier, unknown][] = [] ++ , TOutMutators extends [StoreMutatorIdentifier, unknown][] = [] ++ , TReturn = TState ++ > = ++ ... +``` + +#### Migration + +If you're not using `StateCreator` for authoring middlewares then you likely won't be using `StateCreator`. But in any case if you still are, then can try the following things or open an issue for help... + +- Replace `StateCreator` with `StateCreator` +- Replace `StateCreator` with `StateCreator` where `Mutators` that the store will have eg `StateCreator`. You can learn more about how to write mutators in the updated readme. + +If you're using `StateCreator` to author middlewares then please check the updated readme to see a guide on how to author a middleware. From b76e36c912a7c85d4c538f5bf08a971c669d9896 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 27 Mar 2022 19:18:45 +0530 Subject: [PATCH 102/144] fix devtools merge with base Co-authored-by: Daishi Kato --- src/middleware/devtools.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index bfe9d1a120..63961e8026 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -100,8 +100,7 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { let extensionConnector try { extensionConnector = - (window as any).__REDUX_DEVTOOLS_EXTENSION__ || - (window as any).top.__REDUX_DEVTOOLS_EXTENSION__ + window.__REDUX_DEVTOOLS_EXTENSION__ } catch { // ignored } From 719d184eeda1b2501376d790bc0abc4179954ee6 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Sun, 27 Mar 2022 19:24:52 +0530 Subject: [PATCH 103/144] add a test case for persist with partialize option --- tests/middlewareTypes.test.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/middlewareTypes.test.tsx b/tests/middlewareTypes.test.tsx index e6a18c6b55..45be8f9c40 100644 --- a/tests/middlewareTypes.test.tsx +++ b/tests/middlewareTypes.test.tsx @@ -200,6 +200,33 @@ describe('counter state spec (single middleware)', () => { ) }) + it('persist with partialize', () => { + const useStore = create()( + persist( + (set, get) => ({ + count: 1, + inc: () => set({ count: get().count + 1 }, false), + }), + { name: 'prefix', partialize: (s) => s.count } + ) + ) + const TestComponent = () => { + useStore((s) => s.count) * 2 + useStore((s) => s.inc)() + useStore().count * 2 + useStore().inc() + useStore.getState().count * 2 + useStore.getState().inc() + useStore.persist.hasHydrated() + useStore.persist.setOptions({ + // @ts-expect-error to test if the partialized state is inferred as number + partialize: () => 'not-a-number', + }) + return <> + } + TestComponent + }) + it('persist without custom api (#638)', () => { const useStore = create()( persist( From c71afb6074e0384976ae1a81dca0bb91e4ea06ec Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 30 Mar 2022 19:33:27 +0530 Subject: [PATCH 104/144] update readme --- readme.md | 273 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 196 insertions(+), 77 deletions(-) diff --git a/readme.md b/readme.md index 2ec679504f..ee67e59ecf 100644 --- a/readme.md +++ b/readme.md @@ -196,30 +196,6 @@ const unsub4 = useStore.subscribe(state => [state.paw, state.fur], console.log, const unsub5 = useStore.subscribe(state => state.paw, console.log, { fireImmediately: true }) ``` -
-How to type store with `subscribeWithSelector` in TypeScript - -```ts -import create, { GetState, SetState } from 'zustand' -import { StoreApiWithSubscribeWithSelector, subscribeWithSelector } from 'zustand/middleware' - -type BearState = { - paw: boolean - snout: boolean - fur: boolean -} -const useStore = create< - BearState, - SetState, - GetState, - StoreApiWithSubscribeWithSelector ->(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true }))) -``` - -For more complex typing with multiple middlewares, -Please refer [middlewareTypes.test.tsx](./tests/middlewareTypes.test.tsx). -
- ## Using zustand without React Zustands core can be imported and used without the React dependency. The only difference is that the create function does not return a hook, but the api utilities. @@ -306,36 +282,6 @@ const useStore = create( ) ``` -
-How to pipe middlewares - -```js -import create from "zustand" -import produce from "immer" -import pipe from "ramda/es/pipe" - -/* log and immer functions from previous example */ -/* you can pipe as many middlewares as you want */ -const createStore = pipe(log, immer, create) - -const useStore = createStore(set => ({ - bears: 1, - increasePopulation: () => set(state => ({ bears: state.bears + 1 })) -})) - -export default useStore -``` - -For a TS example see the following [discussion](https://github.com/pmndrs/zustand/discussions/224#discussioncomment-118208) -
- -
-How to type immer middleware in TypeScript - -There is a reference implementation in [middlewareTypes.test.tsx](./tests/middlewareTypes.test.tsx) with some use cases. -You can use any simplified variant based on your requirement. -
- ## Persist middleware You can persist your store's data using any kind of storage. @@ -586,43 +532,216 @@ const Component = () => { ```
-## Typing your store and `combine` middleware +## TypeScript Usage -```tsx -// You can use `type` -type BearState = { - bears: number - increase: (by: number) => void -} +### Basic usage + +When using TypeScript you just have to make a tiny change that instead of writing `create(...)` you'll have to write `create()(...)` where `T` would be type of the state so as to annotate it. Example... + +```ts +import create from "zustand"; -// Or `interface` interface BearState { bears: number - increase: (by: number) => void + increase: () => } -// And it is going to work for both -const useStore = create(set => ({ +create()((set) => ({ bears: 0, - increase: (by) => set(state => ({ bears: state.bears + by })), + increase: () => set((state) => ({ bears: state.bears + 1 })), })) ``` -Or, use `combine` and let tsc infer types. This merges two states shallowly. +
+ Why can't we just simply infer the type from initial state? -```tsx -import { combine } from 'zustand/middleware' + **TLDR**: Because state generic `T` is invariant. + + Consider this minimal version `create`... -const useStore = create( - combine( - { bears: 0 }, - (set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })) }) - ), -) -``` + ```ts + declare const create: (f: (get: () => T) => T) => T + + let x = create(get => ({ + foo: 0, + bar: () => get() + })) + // `x` is inferred as `unknown` instead of + // interface X { + // foo: number, + // bar: () => X + // } + ``` -Typing with multiple middleware might require some TypeScript knowledge. Refer some working examples in [middlewareTypes.test.tsx](./tests/middlewareTypes.test.tsx). + Here if you look at the type of `f` in `create` ie `(get: () => T) => T` it "gives" `T` as it returns `T` but then it also "takes" `T` via `get` so where does `T` come from TypeScript thinks... It's a like that chicken or egg problem. At the end TypeScript gives up and infers `T` as `unknown`. + + So as long as the generic to be inferred is invariant TypeScript won't be able to infer it. Another simple example would be this... + + ```ts + declare const createFoo: (f: (t: T) => T) => T + let x = createFoo(_ => "hello") + ``` + + Here again `x` is `unknown` instead of `string`. + Now one can argue it's impossible to write an implementation for `createFoo`, and that's true. But then it's also impossible to write Zustand's `create`... Wait but Zustand exists? So what do I mean by that? + + The thing is Zustand is lying in it's type, the simplest way to prove it by showing unsoundness. Consider this example... + + ```ts + import create from "zustand/vanilla" + + create<{ foo: number }>()((_, get) => ({ + foo: get().foo, + })) + ``` + + This code compiles, but guess what happens when you run it? You'll get an exception "Uncaught TypeError: Cannot read properties of undefined (reading 'foo') because after all `get` would return `undefined` before the initial state is created (hence kids don't call `get` when creating the initial state). But the types tell that get is `() => { foo: number }` which is exactly the lie I was taking about, `get` is that eventually but first it's `() => undefined`. + + Okay we're quite deep in the rabbit hole haha, long story short zustand has a bit crazy runtime behavior that can't be typed in a sound way and inferrable way. We could make it inferrable with the right TypeScript features that don't exist today. And hey that tiny bit of unsoundness is not a problem. +
+ +
+ Why that currying `()(...)`? + + **TLDR**: It's a workaround for [microsoft/TypeScript#10571](https://github.com/microsoft/TypeScript/issues/10571). + + Imagine you have a scenario like this... + + ```ts + declare const withError: (p: Promise) => + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + declare const doSomething: () => Promise + + const main = async () => { + let [error, value] = await withError(doSomething()) + } + ``` + + Here `T` is inferred as `string` and `E` is inferred as `unknown`. Now for some reason you want to annotate `E` as `Foo` because you're certain what shape of error `doSomething()` would throw. But too bad you can't do that, you can either pass all generics or none. So now along with annotating `E` as `Foo` you'll also have to annotate `T` as `string` which gets inferred anyway. So what to do? What you do is make a curried version of `withError` that does nothing in runtime, it's purpose is to just allow you annotate `E`... + + ```ts + declare const withError: { + (): (p: Promise) => + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + (p: Promise): + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + } + declare const doSomething: () => Promise + interface Foo { bar: string } + + const main = async () => { + let [error, value] = await withError()(doSomething()) + } + ``` + + And now `T` gets inferred and you get to annotate `E` too. Zustand has the same use case we want to annotate the state (the first type parameter) but allow the rest type parameters to get inferred. +
+ +### Using middlewares + +You don't have to do anything special to use middlewares in TypeScript, just make sure you're using them immediately inside `create` so as to make the contextual inference work. But if you're something even remotely fancy like this... + +```js +import create from "zustand" +import { devtools, persist } from "zustand/middleware" + +const myMiddlewares = f => devtools(persist(f)) + +create<{ bears: number }>(myMiddlewares(() => ({ bears: 0 }))) +``` + +Then it'll be a problematic to type `myMiddlewares`. Instead just keep it simple... + +```ts +import create from "zustand" +import { devtools, persist } from "zustand/middleware" + +create<{ bears: number }>(devtools(persist(() => ({ bears: 0 }))) +``` + +Now you don't have to do any extra typing work. + +### Authoring middlewares and advanced usage + +Imagine you had to write this hypothetical middleware... + +```js +import create from "zustand" + +const foo = (f, bar) => (set, get, store) => { + let s = f(set, get, store) + store.foo = bar + return s; +} + +let store = create(foo(() => ({ bears: 0 }), "hello")) +console.log(store.foo.toUpperCase()) +``` + +Yes, if you didn't know Zustand middlewares do and are allowed to mutate the store. But how could we possibly encode the mutation on the type-level? That is to say how could do we type `foo` so that this code compiles? + +For an usual statically typed language this is impossible, but thanks to TypeScript, Zustand has something called an "higher kinded mutator" that makes this possible. If you're dealing with complex type problems like typing a middleware or using the `StateCreator` type, then you'll have to understand this implementation detail, for that check out [#710](https://github.com/pmndrs/zustand/issues/710). + +
+ If you're eager to know what the answer is to this particular problem then it's the following... + + ```js + import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + + type Foo = + < T extends State + , A + , Mps extends [StoreMutatorIdentifier, unknown][] = [] + , Mcs extends [StoreMutatorIdentifier, unknown][] = [] + > + ( f: StateCreator + , bar: A + ) => + StateCreator + + declare module 'zustand' { + interface StoreMutators { + foo: Write { foo: A }> + } + } + + type FooImpl = + + ( f: PopArgument> + , bar: A + ) => PopArgument> + + const fooImpl: FooImpl = (f, bar) => (set, get, _store) => { + type T = ReturnType + type A = typeof bar + + let s = f(set, get, _store) + let store = _store as Mutate, [['foo', A]]> + store.foo = bar + return s + } + + export const foo = fooImpl as unknown as Foo + + type PopArgument unknown> = + T extends (...a: [...infer A, infer _]) => infer R + ? (...a: A) => R + : never + + type Write = + Omit & U + + type Cast = + T extends U ? T : U; + + // --- + + let store = create(foo(() => ({ bears: 0 }), "hello")) + console.log(store.foo.toUpperCase()) + ``` +
+ ## Best practices * You may wonder how to organize your code for better maintenance: [Splitting the store into seperate slices](https://github.com/pmndrs/zustand/wiki/Splitting-the-store-into-separate-slices). From 826ef8e79bc15e6210f53a491777eed3c051dc02 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 30 Mar 2022 19:57:11 +0530 Subject: [PATCH 105/144] fix lint --- src/middleware/devtools.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 63961e8026..8ae2f7b9ba 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -99,8 +99,7 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { let extensionConnector try { - extensionConnector = - window.__REDUX_DEVTOOLS_EXTENSION__ + extensionConnector = window.__REDUX_DEVTOOLS_EXTENSION__ } catch { // ignored } From 2873f2279d14e33b44153a40846e641f17e90d04 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 30 Mar 2022 20:10:39 +0530 Subject: [PATCH 106/144] immer: mutate `store.setState` --- src/middleware/immer.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/middleware/immer.ts b/src/middleware/immer.ts index d6fb6d51f8..9cb68d03d1 100644 --- a/src/middleware/immer.ts +++ b/src/middleware/immer.ts @@ -51,17 +51,16 @@ type ImmerImpl = ( const immerImpl: ImmerImpl = (initializer) => (set, get, store) => { type T = ReturnType - return initializer( - (updater, replace) => { - const nextState = ( - typeof updater === 'function' ? produce(updater as any) : updater - ) as ((s: T) => T) | T | Partial + const immerSetState: typeof set = (updater, replace) => { + const nextState = ( + typeof updater === 'function' ? produce(updater as any) : updater + ) as ((s: T) => T) | T | Partial - return set(nextState as any, replace) - }, - get, - store - ) + return set(nextState as any, replace) + } + const s = initializer(immerSetState, get, store) + store.setState = immerSetState + return s } export const immer = immerImpl as unknown as Immer From a539f54abbee5ea156e5b648a8a932b823dd67b8 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 30 Mar 2022 20:28:40 +0530 Subject: [PATCH 107/144] fix devtools merge with base --- src/middleware/devtools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index 8ae2f7b9ba..9ade8e51e9 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -92,7 +92,7 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => { const devtoolsOptions = options === undefined - ? { name: undefined, anonymousActionType: undefined } + ? {} : typeof options === 'string' ? { name: options } : options From 26f04d4b8d72eb1501d8775361dfe820b3f2e348 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 4 Apr 2022 00:01:50 +0530 Subject: [PATCH 108/144] immer: fix mutations order --- src/middleware/immer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/middleware/immer.ts b/src/middleware/immer.ts index 9cb68d03d1..c9051970aa 100644 --- a/src/middleware/immer.ts +++ b/src/middleware/immer.ts @@ -51,16 +51,16 @@ type ImmerImpl = ( const immerImpl: ImmerImpl = (initializer) => (set, get, store) => { type T = ReturnType - const immerSetState: typeof set = (updater, replace) => { + + store.setState = (updater, replace) => { const nextState = ( typeof updater === 'function' ? produce(updater as any) : updater ) as ((s: T) => T) | T | Partial return set(nextState as any, replace) } - const s = initializer(immerSetState, get, store) - store.setState = immerSetState - return s + + return initializer(store.setState, get, store) } export const immer = immerImpl as unknown as Immer From 968ca5df2fd7756b638785962ce7e14bebd513ae Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 4 Apr 2022 00:05:20 +0530 Subject: [PATCH 109/144] changes in readme --- readme.md | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/readme.md b/readme.md index ee67e59ecf..0c29f010ad 100644 --- a/readme.md +++ b/readme.md @@ -543,12 +543,12 @@ import create from "zustand"; interface BearState { bears: number - increase: () => + increase: (by: number) => void } -create()((set) => ({ +const useStore = create()((set) => ({ bears: 0, - increase: () => set((state) => ({ bears: state.bears + 1 })), + increase: (by) => set((state) => ({ bears: state.bears + by })), })) ``` @@ -562,7 +562,7 @@ create()((set) => ({ ```ts declare const create: (f: (get: () => T) => T) => T - let x = create(get => ({ + const x = create(get => ({ foo: 0, bar: () => get() })) @@ -579,7 +579,7 @@ create()((set) => ({ ```ts declare const createFoo: (f: (t: T) => T) => T - let x = createFoo(_ => "hello") + const x = createFoo(_ => "hello") ``` Here again `x` is `unknown` instead of `string`. @@ -591,7 +591,7 @@ create()((set) => ({ ```ts import create from "zustand/vanilla" - create<{ foo: number }>()((_, get) => ({ + const useStore = create<{ foo: number }>()((_, get) => ({ foo: get().foo, })) ``` @@ -640,27 +640,41 @@ create()((set) => ({ ### Using middlewares -You don't have to do anything special to use middlewares in TypeScript, just make sure you're using them immediately inside `create` so as to make the contextual inference work. But if you're something even remotely fancy like this... +You don't have to do anything special to use middlewares in TypeScript. -```js +```ts import create from "zustand" import { devtools, persist } from "zustand/middleware" -const myMiddlewares = f => devtools(persist(f)) +interface BearState { + bears: number + increase: (by: number) => void +} -create<{ bears: number }>(myMiddlewares(() => ({ bears: 0 }))) +const useStore = create()(devtools(persist((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +})))) ``` -Then it'll be a problematic to type `myMiddlewares`. Instead just keep it simple... +Just make sure you're using them immediately inside `create` so as to make the contextual inference work. Doing something even remotely fancy like the following `myMiddlewares` would require more advanced types. ```ts import create from "zustand" import { devtools, persist } from "zustand/middleware" -create<{ bears: number }>(devtools(persist(() => ({ bears: 0 }))) -``` +const myMiddlewares = f => devtools(persist(f)) -Now you don't have to do any extra typing work. +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create()(myMiddlewares((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +}))) +``` ### Authoring middlewares and advanced usage @@ -670,12 +684,11 @@ Imagine you had to write this hypothetical middleware... import create from "zustand" const foo = (f, bar) => (set, get, store) => { - let s = f(set, get, store) store.foo = bar - return s; + return f(set, get, store); } -let store = create(foo(() => ({ bears: 0 }), "hello")) +const useStore = create(foo(() => ({ bears: 0 }), "hello")) console.log(store.foo.toUpperCase()) ``` @@ -716,10 +729,9 @@ For an usual statically typed language this is impossible, but thanks to TypeScr type T = ReturnType type A = typeof bar - let s = f(set, get, _store) - let store = _store as Mutate, [['foo', A]]> + const store = _store as Mutate, [['foo', A]]> store.foo = bar - return s + return f(set, get, _store) } export const foo = fooImpl as unknown as Foo @@ -737,7 +749,7 @@ For an usual statically typed language this is impossible, but thanks to TypeScr // --- - let store = create(foo(() => ({ bears: 0 }), "hello")) + const useStore = create(foo(() => ({ bears: 0 }), "hello")) console.log(store.foo.toUpperCase()) ``` From 7b4793fe2b13d335a3555cfe87abf52d61c0fcab Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 4 Apr 2022 00:28:04 +0530 Subject: [PATCH 110/144] move and rename v4 migration md --- v4.0.0-rc-release-notes.md => docs/v4-migration.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename v4.0.0-rc-release-notes.md => docs/v4-migration.md (100%) diff --git a/v4.0.0-rc-release-notes.md b/docs/v4-migration.md similarity index 100% rename from v4.0.0-rc-release-notes.md rename to docs/v4-migration.md From d63508206cfb905bed0b44ea0aca0e789747f5a1 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 4 Apr 2022 01:18:07 +0530 Subject: [PATCH 111/144] add `combine` usage in readme --- readme.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/readme.md b/readme.md index 0c29f010ad..b16b24c3f4 100644 --- a/readme.md +++ b/readme.md @@ -638,6 +638,25 @@ const useStore = create()((set) => ({ And now `T` gets inferred and you get to annotate `E` too. Zustand has the same use case we want to annotate the state (the first type parameter) but allow the rest type parameters to get inferred. +Alternatively you can also use `combine` which infers the state instead of you having to type it... + +```ts +import create from "zustand" +import { combine } from "zustand/middleware" + +const useStore = create(combine({ bears: 0 }, (set, get, store) => ({ + increase: (by: number) => set((state) => ({ bears: state.bears + by })), +})) +``` + +
+ But there's a little cost... + + We achieve the inference by lying a little in the types of `set`, `get` and `store` that you receive as parameters. The lie is that they're typed in a way as if the state is the state first parameter of `combine` only when in fact the state if the shallow-merge (`{ ...a, ...b }`) of both states the one in first parameter and the one in the second parameter. So for example `get` from the received parameter has type `() => { bears: number }` and that's a lie it should be `() => { bears: number, increase: (by: number) => void }`. It's not a lie lie because `{ bears: number }` is still a subtype `{ bears: number, increase: (by: number) => void }`, so there's not much to worry, the types are a little less specific but not really "incorrect". And `useStore` still has the correct types, ie for example `useStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. + + So `combine` trades-off a little type-safety for the convience of not having to write a type for state. Hence you should use `combine` accordingly, usually it's not a big deal and it's okay to use it. +
+ ### Using middlewares You don't have to do anything special to use middlewares in TypeScript. From 810b41883cffcac2124f3f861bd9e2c8250cf2c0 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 4 Apr 2022 03:24:55 +0530 Subject: [PATCH 112/144] typos --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b16b24c3f4..3626d2f44d 100644 --- a/readme.md +++ b/readme.md @@ -652,7 +652,7 @@ const useStore = create(combine({ bears: 0 }, (set, get, store) => ({
But there's a little cost... - We achieve the inference by lying a little in the types of `set`, `get` and `store` that you receive as parameters. The lie is that they're typed in a way as if the state is the state first parameter of `combine` only when in fact the state if the shallow-merge (`{ ...a, ...b }`) of both states the one in first parameter and the one in the second parameter. So for example `get` from the received parameter has type `() => { bears: number }` and that's a lie it should be `() => { bears: number, increase: (by: number) => void }`. It's not a lie lie because `{ bears: number }` is still a subtype `{ bears: number, increase: (by: number) => void }`, so there's not much to worry, the types are a little less specific but not really "incorrect". And `useStore` still has the correct types, ie for example `useStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. + We achieve the inference by lying a little in the types of `set`, `get` and `store` that you receive as parameters. The lie is that they're typed in a way as if the state is the first parameter only when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. So for example `get` from the second parameter has type `() => { bears: number }` and that's a lie as it should be `() => { bears: number, increase: (by: number) => void }`. It's not a lie lie because `{ bears: number }` is still a subtype `{ bears: number, increase: (by: number) => void }`, so there's not much to worry, the types are a little less specific but not really "incorrect". And `useStore` still has the correct type, ie for example `useStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. So `combine` trades-off a little type-safety for the convience of not having to write a type for state. Hence you should use `combine` accordingly, usually it's not a big deal and it's okay to use it.
From 4f9d13fce5a88b09aacd9c8a2c14b0a189c8f474 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Mon, 4 Apr 2022 21:22:52 +0530 Subject: [PATCH 113/144] create separate md for typescript, add common recipes --- docs/typescript.md | 290 +++++++++++++++++++++++++++++++++++++++++++++ readme.md | 228 +---------------------------------- 2 files changed, 294 insertions(+), 224 deletions(-) create mode 100644 docs/typescript.md diff --git a/docs/typescript.md b/docs/typescript.md new file mode 100644 index 0000000000..f09b729991 --- /dev/null +++ b/docs/typescript.md @@ -0,0 +1,290 @@ +# TypeScript Guide + +## Basic usage + +When using TypeScript you just have to make a tiny change that instead of writing `create(...)` you'll have to write `create()(...)` where `T` would be type of the state so as to annotate it. Example... + +```ts +import create from "zustand" + +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create()((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +})) +``` + +
+ Why can't we just simply infer the type from initial state? + + **TLDR**: Because state generic `T` is invariant. + + Consider this minimal version `create`... + + ```ts + declare const create: (f: (get: () => T) => T) => T + + const x = create(get => ({ + foo: 0, + bar: () => get() + })) + // `x` is inferred as `unknown` instead of + // interface X { + // foo: number, + // bar: () => X + // } + ``` + + Here if you look at the type of `f` in `create` ie `(get: () => T) => T` it "gives" `T` as it returns `T` but then it also "takes" `T` via `get` so where does `T` come from TypeScript thinks... It's a like that chicken or egg problem. At the end TypeScript gives up and infers `T` as `unknown`. + + So as long as the generic to be inferred is invariant TypeScript won't be able to infer it. Another simple example would be this... + + ```ts + declare const createFoo: (f: (t: T) => T) => T + const x = createFoo(_ => "hello") + ``` + + Here again `x` is `unknown` instead of `string`. + + Now one can argue it's impossible to write an implementation for `createFoo`, and that's true. But then it's also impossible to write Zustand's `create`... Wait but Zustand exists? So what do I mean by that? + + The thing is Zustand is lying in it's type, the simplest way to prove it by showing unsoundness. Consider this example... + + ```ts + import create from "zustand/vanilla" + + const useStore = create<{ foo: number }>()((_, get) => ({ + foo: get().foo, + })) + ``` + + This code compiles, but guess what happens when you run it? You'll get an exception "Uncaught TypeError: Cannot read properties of undefined (reading 'foo') because after all `get` would return `undefined` before the initial state is created (hence kids don't call `get` when creating the initial state). But the types tell that get is `() => { foo: number }` which is exactly the lie I was taking about, `get` is that eventually but first it's `() => undefined`. + + Okay we're quite deep in the rabbit hole haha, long story short zustand has a bit crazy runtime behavior that can't be typed in a sound way and inferrable way. We could make it inferrable with the right TypeScript features that don't exist today. And hey that tiny bit of unsoundness is not a problem. +
+ +
+ Why that currying `()(...)`? + + **TLDR**: It's a workaround for [microsoft/TypeScript#10571](https://github.com/microsoft/TypeScript/issues/10571). + + Imagine you have a scenario like this... + + ```ts + declare const withError: (p: Promise) => + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + declare const doSomething: () => Promise + + const main = async () => { + let [error, value] = await withError(doSomething()) + } + ``` + + Here `T` is inferred as `string` and `E` is inferred as `unknown`. Now for some reason you want to annotate `E` as `Foo` because you're certain what shape of error `doSomething()` would throw. But too bad you can't do that, you can either pass all generics or none. So now along with annotating `E` as `Foo` you'll also have to annotate `T` as `string` which gets inferred anyway. So what to do? What you do is make a curried version of `withError` that does nothing in runtime, it's purpose is to just allow you annotate `E`... + + ```ts + declare const withError: { + (): (p: Promise) => + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + (p: Promise): + Promise<[error: undefined, value: T] | [error: E, value: undefined]> + } + declare const doSomething: () => Promise + interface Foo { bar: string } + + const main = async () => { + let [error, value] = await withError()(doSomething()) + } + ``` + + And now `T` gets inferred and you get to annotate `E` too. Zustand has the same use case we want to annotate the state (the first type parameter) but allow the rest type parameters to get inferred. +
+ +Alternatively you can also use `combine` which infers the state instead of you having to type it... + +```ts +import create from "zustand" +import { combine } from "zustand/middleware" + +const useStore = create(combine({ bears: 0 }, (set, get, store) => ({ + increase: (by: number) => set((state) => ({ bears: state.bears + by })), +})) +``` + +
+ But there's a little cost... + + We achieve the inference by lying a little in the types of `set`, `get` and `store` that you receive as parameters. The lie is that they're typed in a way as if the state is the first parameter only when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. So for example `get` from the second parameter has type `() => { bears: number }` and that's a lie as it should be `() => { bears: number, increase: (by: number) => void }`. It's not a lie lie because `{ bears: number }` is still a subtype `{ bears: number, increase: (by: number) => void }`, so there's not much to worry, the types are a little less specific but not really "incorrect". And `useStore` still has the correct type, ie for example `useStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. + + So `combine` trades-off a little type-safety for the convience of not having to write a type for state. Hence you should use `combine` accordingly, usually it's not a big deal and it's okay to use it. +
+ +## Using middlewares + +You don't have to do anything special to use middlewares in TypeScript. + +```ts +import create from "zustand" +import { devtools, persist } from "zustand/middleware" + +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create()(devtools(persist((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +})))) +``` + +Just make sure you're using them immediately inside `create` so as to make the contextual inference work. Doing something even remotely fancy like the following `myMiddlewares` would require more advanced types. + +```ts +import create from "zustand" +import { devtools, persist } from "zustand/middleware" + +const myMiddlewares = f => devtools(persist(f)) + +interface BearState { + bears: number + increase: (by: number) => void +} + +const useStore = create()(myMiddlewares((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +}))) +``` + +## Authoring middlewares and advanced usage + +Imagine you had to write this hypothetical middleware... + +```js +import create from "zustand" + +const foo = (f, bar) => (set, get, store) => { + store.foo = bar + return f(set, get, store); +} + +const useStore = create(foo(() => ({ bears: 0 }), "hello")) +console.log(store.foo.toUpperCase()) +``` + +Yes, if you didn't know Zustand middlewares do and are allowed to mutate the store. But how could we possibly encode the mutation on the type-level? That is to say how could do we type `foo` so that this code compiles? + +For an usual statically typed language this is impossible, but thanks to TypeScript, Zustand has something called an "higher kinded mutator" that makes this possible. If you're dealing with complex type problems like typing a middleware or using the `StateCreator` type, then you'll have to understand this implementation detail, for that check out [#710](https://github.com/pmndrs/zustand/issues/710). + +If you're eager to know what the answer is to this particular problem then it's [here](#middleware-that-changes-the-store-type). + +## Common recipes + +### Middleware that does not change the store type + +```ts +import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + +type Logger = + < T extends State + , Mps extends [StoreMutatorIdentifier, unknown][] = [] + , Mcs extends [StoreMutatorIdentifier, unknown][] = [] + > + ( f: StateCreator + , name?: string + ) => + StateCreator + +type LoggerImpl = + + ( f: PopArgument> + , name?: string + ) => + PopArgument> + +const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => { + type T = ReturnType + const loggedSet: typeof set = (...a) => { + set(...a) + console.log(...(name ? [`${name}:`] : []), get()) + } + store.setState = loggedState + + return f(loggedSet, get, store) +} + +export const logger = loggerImpl as unknown as Foo + +type PopArgument unknown> = + T extends (...a: [...infer A, infer _]) => infer R + ? (...a: A) => R + : never + +// --- + +const useStore = create()(logger((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), +}), "bear-store")) +``` + +### Middleware that changes the store type + +```js +import create, { State, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi } from "zustand" + +type Foo = + < T extends State + , A + , Mps extends [StoreMutatorIdentifier, unknown][] = [] + , Mcs extends [StoreMutatorIdentifier, unknown][] = [] + > + ( f: StateCreator + , bar: A + ) => + StateCreator + +declare module 'zustand' { + interface StoreMutators { + foo: Write { foo: A }> + } +} + +type FooImpl = + + ( f: PopArgument> + , bar: A + ) => PopArgument> + +const fooImpl: FooImpl = (f, bar) => (set, get, _store) => { + type T = ReturnType + type A = typeof bar + + const store = _store as Mutate, [['foo', A]]> + store.foo = bar + return f(set, get, _store) +} + +export const foo = fooImpl as unknown as Foo + +type PopArgument unknown> = + T extends (...a: [...infer A, infer _]) => infer R + ? (...a: A) => R + : never + +type Write = + Omit & U + +type Cast = + T extends U ? T : U; + +// --- + +const useStore = create(foo(() => ({ bears: 0 }), "hello")) +console.log(store.foo.toUpperCase()) +``` diff --git a/readme.md b/readme.md index 3626d2f44d..7c8cf4cefa 100644 --- a/readme.md +++ b/readme.md @@ -527,139 +527,14 @@ const Component = () => { >