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

[v4-alpha] use mutable source #422

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5e8349c
[v4-experimental] use mutable source (#157)
dai-shi Aug 18, 2020
2dae0fc
Merge branch 'master' into v4-experimental
drcmda Aug 18, 2020
77f7baf
merge master
dai-shi Aug 27, 2020
04e7d37
Merge branch 'master' into v4-experimental
dai-shi Aug 29, 2020
9166374
Merge branch 'master' into v4-experimental
dai-shi Aug 29, 2020
a6d5495
specify sandboxes in ci.json
dai-shi Aug 29, 2020
199702e
specify sandboxes in ci.json
dai-shi Aug 29, 2020
5db9727
specify sandboxes in ci.json
dai-shi Aug 29, 2020
ec65058
Merge branch 'master' into v4-experimental
dai-shi Aug 29, 2020
ab1dc3e
merge master
dai-shi Sep 1, 2020
72fcfbb
merge master
dai-shi Sep 4, 2020
360d0ec
merge master
dai-shi Sep 8, 2020
f9ae43e
merge master
dai-shi Sep 10, 2020
3c991c0
lock experimental version
dai-shi Sep 10, 2020
a3cfc89
merge master
dai-shi Sep 12, 2020
7c46e3e
merge master
dai-shi Oct 5, 2020
51594ed
merge master
dai-shi Dec 19, 2020
adf3c7c
merge master
dai-shi Feb 28, 2021
22ddeb4
wait a little bit?
dai-shi Feb 28, 2021
ca5000b
merge master
dai-shi Apr 8, 2021
40bbf05
merge master
dai-shi May 16, 2021
328cf14
merge master
dai-shi Jun 9, 2021
54d7642
migrates to react@alpha
dai-shi Jun 9, 2021
20df287
fix prettier
dai-shi Jun 9, 2021
bdfb295
merge master
dai-shi Jul 23, 2021
7d4f014
merge main
dai-shi Aug 19, 2021
86db714
fix test
dai-shi Aug 19, 2021
665b644
fix lint
dai-shi Aug 19, 2021
3b036ab
fix subscribe, why it seemed working?
dai-shi Aug 20, 2021
a7b0ea4
update size snapshot
dai-shi Aug 20, 2021
60616c7
v4.0.0-alpha.1
dai-shi Aug 20, 2021
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
16 changes: 8 additions & 8 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"index.js": {
"bundled": 4684,
"minified": 1888,
"gzipped": 846
"bundled": 3513,
"minified": 1263,
"gzipped": 597
},
"index.mjs": {
"bundled": 3993,
"minified": 1614,
"gzipped": 786,
"bundled": 2840,
"minified": 1019,
"gzipped": 525,
"treeshaked": {
"rollup": {
"code": 124,
"code": 14,
"import_statements": 14
},
"webpack": {
"code": 1144
"code": 998
}
}
},
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "zustand",
"private": true,
"version": "3.5.9",
"version": "4.0.0-alpha.1",
"publishConfig": {
"tag": "alpha"
},
"description": "🐻 Bear necessities for state management in React",
"main": "./index.js",
"types": "./index.d.ts",
Expand Down Expand Up @@ -151,16 +154,16 @@
"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.2",
"rollup-plugin-esbuild": "^4.5.0",
"rollup-plugin-size-snapshot": "^0.12.0",
"shx": "^0.3.3",
"typescript": "^4.3.5"
},
"peerDependencies": {
"react": ">=16.8"
"react": "alpha"
},
"peerDependenciesMeta": {
"react": {
Expand Down
138 changes: 44 additions & 94 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { useEffect, useLayoutEffect, useReducer, useRef } from 'react'
/// <reference types="react/next" />

import {
unstable_createMutableSource as createMutableSource,
useMemo,
unstable_useMutableSource as useMutableSource,
} from 'react'
import createImpl, {
Destroy,
EqualityChecker,
Expand All @@ -12,15 +18,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 extends State> {
(): T
<U>(selector: StateSelector<T, U>, equalityFn?: EqualityChecker<U>): U
Expand All @@ -36,102 +33,55 @@ export default function create<TState extends State>(
const api: StoreApi<TState> =
typeof createState === 'function' ? createImpl(createState) : createState

const source = createMutableSource(api, () => api.getState())
const subscribe = (api: StoreApi<TState>, callback: () => void) =>
api.subscribe(callback)

const FUNCTION_SYMBOL = Symbol()
const functionMap = new WeakMap<Function, { [FUNCTION_SYMBOL]: Function }>()

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 = () => {
const getSnapshot = useMemo(() => {
let lastSlice: StateSlice | undefined
return (api: StoreApi<TState>) => {
let slice = lastSlice
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()
slice = selector(api.getState())
if (lastSlice !== undefined && equalityFn(lastSlice, slice)) {
slice = lastSlice
} else {
lastSlice = slice
}
} catch (error) {
erroredRef.current = true
forceUpdate()
// ignore and let react reschedule update
}
// Unfortunately, returning a function is not supported
// https://github.com/facebook/react/issues/18823
if (typeof slice === 'function') {
if (functionMap.has(slice)) {
return functionMap.get(slice)
}
const wrappedFunction = { [FUNCTION_SYMBOL]: slice }
functionMap.set(slice, wrappedFunction)
return wrappedFunction
}
return slice
}
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
}, [selector, equalityFn])
const snapshot = useMutableSource(source, getSnapshot, subscribe)
if (
snapshot &&
(snapshot as { [FUNCTION_SYMBOL]: unknown })[FUNCTION_SYMBOL]
) {
return (snapshot as { [FUNCTION_SYMBOL]: unknown })[FUNCTION_SYMBOL]
}
return snapshot
}

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
}
18 changes: 14 additions & 4 deletions tests/basic.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Component as ClassComponent,
useCallback,
useEffect,
useLayoutEffect,
useState,
Expand Down Expand Up @@ -150,6 +151,7 @@ it('re-renders with useLayoutEffect', async () => {

const container = document.createElement('div')
ReactDOM.render(<Component />, container)
await new Promise((resolve) => setTimeout(resolve, 10))
expect(container.innerHTML).toBe('true')
ReactDOM.unmountComponentAtNode(container)
})
Expand Down Expand Up @@ -219,14 +221,14 @@ it('can update the equality checker', async () => {

// This will cause a re-render due to the equality checker.
act(() => setState({ value: 0 }))
await findByText('renderCount: 2, value: 0')
await findByText('renderCount: 1, value: 0')

// Set an equality checker that always returns true to never re-render.
rerender(<Component equalityFn={() => true} />)

// This will NOT cause a re-render due to the equality checker.
act(() => setState({ value: 1 }))
await findByText('renderCount: 3, value: 0')
await findByText('renderCount: 2, value: 0')
})

it('can call useStore with progressively more arguments', async () => {
Expand Down Expand Up @@ -313,7 +315,8 @@ it('can throw an error in selector', async () => {
act(() => {
setState({})
})
await findByText('errored')
// in v4 with uMS, we don't use equalityFn in render
// await findByText('errored')
})

it('can throw an error in equality checker', async () => {
Expand Down Expand Up @@ -357,7 +360,8 @@ it('can throw an error in equality checker', async () => {
act(() => {
setState({})
})
await findByText('errored')
// in v4 with uMS, we don't use equalityFn in render
// await findByText('errored')
})

it('can get the store', () => {
Expand Down Expand Up @@ -428,6 +432,7 @@ it('only calls selectors when necessary', async () => {
const useStore = create<State>(() => ({ a: 0, b: 0 }))
const { setState } = useStore
let inlineSelectorCallCount = 0
let callbackSelectorCallCount = 0
let staticSelectorCallCount = 0

function staticSelector(s: State) {
Expand All @@ -437,25 +442,30 @@ it('only calls selectors when necessary', async () => {

function Component() {
useStore((s) => (inlineSelectorCallCount++, s.b))
useStore(useCallback((s) => (callbackSelectorCallCount++, s.b), []))
useStore(staticSelector)
return (
<>
<div>inline: {inlineSelectorCallCount}</div>
<div>callback: {callbackSelectorCallCount}</div>
<div>static: {staticSelectorCallCount}</div>
</>
)
}

const { rerender, findByText } = render(<Component />)
await findByText('inline: 1')
await findByText('callback: 1')
await findByText('static: 1')

rerender(<Component />)
await findByText('inline: 2')
await findByText('callback: 1')
await findByText('static: 1')

act(() => setState({ a: 1, b: 1 }))
await findByText('inline: 4')
await findByText('callback: 2')
await findByText('static: 2')
})

Expand Down
Loading