Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: use-sync-external-store #550

Merged
merged 66 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
cf511cf
imaginary code that uses uSES
dai-shi Aug 31, 2021
5570b10
revert backward compatibility code as this is not going to be v4
dai-shi Aug 31, 2021
b03b226
use use-sync-external-store
dai-shi Sep 4, 2021
8112285
revert to react 17
dai-shi Sep 4, 2021
50f2963
handle error by our own
dai-shi Sep 4, 2021
94d9d4a
v4.0.0-alpha.2
dai-shi Sep 4, 2021
d36611b
fix&refactor a bit
dai-shi Sep 6, 2021
453e59a
Merge branch 'main' into use-sync-external-store
dai-shi Sep 14, 2021
ef89673
update uSES experimental package
dai-shi Sep 14, 2021
d61a0b7
remove error propagation hack
dai-shi Sep 14, 2021
ec117e6
update size snapshot
dai-shi Sep 14, 2021
e2081c8
update uSES and add dts
dai-shi Sep 27, 2021
1fee70e
split react.ts and no export wild
dai-shi Sep 27, 2021
9aaeb5e
split useStore impl
dai-shi Sep 27, 2021
a914052
context to follow the new api, export wild again
dai-shi Sep 27, 2021
9021456
v4.0.0-alpha.3
dai-shi Sep 27, 2021
b9a70f2
add missing await
dai-shi Sep 28, 2021
98c59d3
merge main
dai-shi Sep 28, 2021
c9c8ae9
merge main
dai-shi Oct 1, 2021
cc391bc
update uSES
dai-shi Oct 1, 2021
bef6f81
update uSES
dai-shi Oct 3, 2021
d78e91f
Merge branch 'main' into use-sync-external-store
dai-shi Oct 3, 2021
54a5b8c
uses uSES extra!
dai-shi Oct 4, 2021
66fc5a7
Merge branch 'main' into use-sync-external-store
dai-shi Oct 4, 2021
1ee93aa
v4.0.0-alpha.3
dai-shi Oct 4, 2021
c643ebf
merge main
dai-shi Oct 7, 2021
a20e4d1
merge main
dai-shi Oct 19, 2021
56d512a
merge main
dai-shi Oct 21, 2021
3b03362
merge main
dai-shi Oct 27, 2021
24d9750
update uSES
dai-shi Oct 27, 2021
f75e868
fix update uSES
dai-shi Oct 27, 2021
2ed60eb
v4.0.0-alpha.5
dai-shi Oct 27, 2021
9675378
merge main
dai-shi Oct 30, 2021
8fad667
add useDebugValue
dai-shi Oct 31, 2021
7575357
update uSES
dai-shi Oct 31, 2021
e51f75a
update uSES types
dai-shi Nov 2, 2021
7941091
merge main
dai-shi Nov 2, 2021
e7adbbf
update uSES
dai-shi Nov 2, 2021
5812d0e
v4.0.0-alpha.6
dai-shi Nov 2, 2021
9690a11
merge main
dai-shi Nov 4, 2021
f037a2a
fix(readme): remove memoization section which is no longer valid with…
dai-shi Nov 4, 2021
8339d52
feat(readme): add new createStore/useStore usage
dai-shi Nov 4, 2021
2e5ab21
merge main
dai-shi Nov 9, 2021
37119f5
update useSES
dai-shi Nov 9, 2021
835684a
update uSES and deps
dai-shi Nov 15, 2021
20abedf
Merge branch 'main' into use-sync-external-store
dai-shi Nov 15, 2021
e91bde6
v4.0.0-alpha.7
dai-shi Nov 15, 2021
c009886
merge main
dai-shi Nov 16, 2021
be18127
merge main
dai-shi Nov 22, 2021
9280262
update uSES
dai-shi Nov 22, 2021
c6acc59
merge main
dai-shi Dec 3, 2021
303fd82
update uSES
dai-shi Dec 7, 2021
0d4e0d8
shave bytes
dai-shi Dec 7, 2021
445bc8f
Merge branch 'main' into use-sync-external-store
dai-shi Dec 7, 2021
dd57cb0
Merge branch 'main' into use-sync-external-store
dai-shi Dec 24, 2021
76b1d4c
update uSES
dai-shi Dec 24, 2021
90fd7ee
Merge branch 'main' into use-sync-external-store
dai-shi Feb 11, 2022
ff7358a
Merge branch 'main' into use-sync-external-store
dai-shi Feb 28, 2022
92a7094
fix yarn lock
dai-shi Feb 28, 2022
64536ee
temporary fix #829
dai-shi Feb 28, 2022
421454a
Merge branch 'main' into use-sync-external-store
dai-shi Mar 4, 2022
33ac559
uSES rc.1
dai-shi Mar 4, 2022
773e2e2
getServerState for #886, no types yet
dai-shi Mar 31, 2022
90ef5c1
uSES v1
dai-shi Apr 5, 2022
748caeb
Merge branch 'main' into use-sync-external-store
dai-shi Apr 7, 2022
1be05d1
Merge branch 'main' into use-sync-external-store
dai-shi Apr 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@
"tests/**/*.{js,ts,tsx}"
]
},
"dependencies": {
"use-sync-external-store": "1.0.0"
},
"devDependencies": {
"@babel/core": "^7.17.9",
"@babel/plugin-external-helpers": "^7.16.7",
Expand All @@ -147,6 +150,7 @@
"@types/jest": "^27.4.1",
"@types/react": "^17.0.43",
"@types/react-dom": "^17.0.14",
"@types/use-sync-external-store": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"concurrently": "^7.1.0",
Expand Down
46 changes: 28 additions & 18 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -468,7 +451,33 @@ devtools(..., { anonymousActionType: 'unknown', ... })

## 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 = () => (
<StoreContext.Provider value={store}>
...
</StoreContext.Provider>
)

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'
Expand All @@ -490,6 +499,7 @@ const Component = () => {
...
}
```

<details>
<summary>createContext usage in real components</summary>

Expand Down
3 changes: 3 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
66 changes: 29 additions & 37 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {
useMemo,
useRef,
} from 'react'
import { EqualityChecker, State, StateSelector, UseBoundStore } from 'zustand'
import {
EqualityChecker,
State,
StateSelector,
StoreApi,
useStore,
} from 'zustand'

/**
* @deprecated Use `typeof MyContext.useStore` instead.
Expand All @@ -18,35 +24,22 @@ export type UseContextStore<T extends State> = {

function createContext<
TState extends State,
TUseBoundStore extends UseBoundStore<TState> = UseBoundStore<TState>
CustomStoreApi extends StoreApi<TState> = StoreApi<TState>
>() {
const ZustandContext = reactCreateContext<TUseBoundStore | undefined>(
const ZustandContext = reactCreateContext<CustomStoreApi | undefined>(
undefined
)

const Provider = ({
initialStore,
createStore,
children,
}: {
/**
* @deprecated
*/
initialStore?: TUseBoundStore
createStore: () => TUseBoundStore
createStore: () => CustomStoreApi
children: ReactNode
}) => {
const storeRef = useRef<TUseBoundStore>()
const storeRef = useRef<CustomStoreApi>()

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()
}

Expand All @@ -57,50 +50,49 @@ function createContext<
)
}

const useStore: UseContextStore<TState> = <StateSlice>(
const useBoundStore: UseContextStore<TState> = <StateSlice>(
selector?: StateSelector<TState, StateSlice>,
equalityFn = Object.is
equalityFn?: EqualityChecker<StateSlice>
) => {
// 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<TState, StateSlice>,
equalityFn
)
}

const useStoreApi = (): {
getState: TUseBoundStore['getState']
setState: TUseBoundStore['setState']
subscribe: TUseBoundStore['subscribe']
destroy: TUseBoundStore['destroy']
getState: CustomStoreApi['getState']
setState: CustomStoreApi['setState']
subscribe: CustomStoreApi['subscribe']
destroy: CustomStoreApi['destroy']
} => {
// 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,
}
}
Expand Down
127 changes: 28 additions & 99 deletions src/react.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {
useDebugValue,
useEffect,
useLayoutEffect,
useReducer,
useRef,
} from 'react'
import { useDebugValue } from 'react'
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
import createStore, {
EqualityChecker,
GetState,
Expand All @@ -15,14 +10,28 @@ import createStore, {
StoreApi,
} 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 function useStore<T extends State>(api: StoreApi<T>): T
export function useStore<T extends State, U>(
api: StoreApi<T>,
selector: StateSelector<T, U>,
equalityFn?: EqualityChecker<U>
): U
export function useStore<TState extends State, StateSlice>(
api: StoreApi<TState>,
selector: StateSelector<TState, StateSlice> = api.getState as any,
equalityFn?: EqualityChecker<StateSlice>
) {
const slice = useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
// TODO avoid `any` and add type only in react.ts
(api as any).getServerState || api.getState,
selector,
equalityFn
)
useDebugValue(slice)
return slice
}

export type UseBoundStore<
T extends State,
Expand Down Expand Up @@ -62,92 +71,12 @@ function create<
const api: CustomStoreApi =
typeof createState === 'function' ? createStore(createState) : createState

const useStore: any = <StateSlice>(
selector: StateSelector<TState, StateSlice> = api.getState as any,
equalityFn: EqualityChecker<StateSlice> = Object.is
) => {
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<StateSlice>()
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
}, [])

const sliceToReturn = hasNewStateSlice
? (newStateSlice as StateSlice)
: currentSliceRef.current
useDebugValue(sliceToReturn)
return sliceToReturn
}
const useBoundStore: any = (selector?: any, equalityFn?: any) =>
useStore(api, selector, equalityFn)

Object.assign(useStore, api)
Object.assign(useBoundStore, api)

return useStore
return useBoundStore
}

export default create
Loading