Skip to content

Commit 611551c

Browse files
Munawwardai-shi
andauthored
#182 SSR data is shared (#375)
* context utils for SSR * move useIsomorphicLayoutEffect to new file * added build commands * 1. remove the useEffect we dont need anymore 2. wrap context and hook into createZustand() function, but keep defaults * issue #182 - changed name to createContext and useStore + added tests * remove default Provider * use alias to index file * change 'createState' prop to 'initialStore' that accepts useStore func/object from create(). This is needed as store access is needed for merging/memoizing, in next.js integration * updated tests * code review changes * snapshot update * add a section in readme * chore imports and so on Co-authored-by: daishi <[email protected]>
1 parent f43d112 commit 611551c

File tree

6 files changed

+216
-1
lines changed

6 files changed

+216
-1
lines changed

.size-snapshot.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,19 @@
159159
"code": 1147
160160
}
161161
}
162+
},
163+
"context.js": {
164+
"bundled": 814,
165+
"minified": 467,
166+
"gzipped": 282,
167+
"treeshaked": {
168+
"rollup": {
169+
"code": 14,
170+
"import_statements": 14
171+
},
172+
"webpack": {
173+
"code": 998
174+
}
175+
}
162176
}
163177
}

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
"types": "./shallow.d.ts",
3131
"module": "./esm/shallow.js",
3232
"default": "./shallow.js"
33+
},
34+
"./context": {
35+
"types": "./context.d.ts",
36+
"module": "./esm/context.js",
37+
"default": "./context.js"
3338
}
3439
},
3540
"sideEffects": false,
@@ -40,6 +45,7 @@
4045
"build:vanilla": "rollup -c --config-vanilla",
4146
"build:middleware": "rollup -c --config-middleware",
4247
"build:shallow": "rollup -c --config-shallow",
48+
"build:context": "rollup -c --config-context",
4349
"postbuild": "yarn copy",
4450
"eslint": "eslint --fix 'src/**/*.{js,ts,jsx,tsx}'",
4551
"eslint-examples": "eslint --fix 'examples/src/**/*.{js,ts,jsx,tsx}'",
@@ -92,6 +98,13 @@
9298
},
9399
"homepage": "https://github.com/react-spring/zustand",
94100
"jest": {
101+
"rootDir": ".",
102+
"moduleNameMapper": {
103+
"^zustand$": "<rootDir>/src/index.ts"
104+
},
105+
"modulePathIgnorePatterns": [
106+
"dist"
107+
],
95108
"testRegex": "test.(js|ts|tsx)$",
96109
"coverageDirectory": "./coverage/",
97110
"collectCoverage": true,

readme.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,37 @@ const useStore = create(devtools(redux(reducer, initialState)))
410410
devtools takes the store function as its first argument, optionally you can name the store with a second argument: `devtools(store, "MyStore")`, which will be prefixed to your actions.
411411
devtools will only log actions from each separated store unlike in a typical *combined reducers* redux store. See an approach to combining stores https://github.com/pmndrs/zustand/issues/163
412412
413+
## React context
414+
415+
The store created with `create` doesn't require context providers. In some cases, you may want to use contexts for dependncy injection. 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.
416+
417+
```jsx
418+
import create from 'zustand'
419+
import createContext from 'zustand/context'
420+
421+
const { Provider, useStore } = createContext()
422+
423+
const store = create(...)
424+
425+
const App = () => (
426+
<Provider initialStore={store}>
427+
...
428+
</Provider>
429+
)
430+
431+
const Component = () => {
432+
const state = useStore()
433+
const slice = useStore(selector)
434+
...
435+
}
436+
```
437+
438+
Optionally, `createContext` accepts `initialState` to infer store types.
439+
440+
```ts
441+
const { Provider, useStore } = createContext(initialState)
442+
```
443+
413444
## TypeScript
414445
415446
```tsx

src/context.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
ReactNode,
3+
createElement,
4+
createContext as reactCreateContext,
5+
useContext,
6+
useRef,
7+
} from 'react'
8+
import { UseStore } from 'zustand'
9+
import { EqualityChecker, State, StateSelector } from './vanilla'
10+
11+
function createContext<TState extends State>(_initialState?: TState) {
12+
const ZustandContext = reactCreateContext<UseStore<TState> | undefined>(
13+
undefined
14+
)
15+
16+
const Provider = ({
17+
initialStore,
18+
children,
19+
}: {
20+
initialStore: UseStore<TState>
21+
children: ReactNode
22+
}) => {
23+
const storeRef = useRef<UseStore<TState>>()
24+
25+
if (!storeRef.current) {
26+
storeRef.current = initialStore
27+
}
28+
29+
return createElement(
30+
ZustandContext.Provider,
31+
{ value: storeRef.current },
32+
children
33+
)
34+
}
35+
36+
const useStore = <StateSlice>(
37+
selector?: StateSelector<TState, StateSlice>,
38+
equalityFn: EqualityChecker<StateSlice> = Object.is
39+
) => {
40+
// ZustandContext value is guaranteed to be stable.
41+
const useProviderStore = useContext(ZustandContext)
42+
if (!useProviderStore) {
43+
throw new Error(
44+
'Seems like you have not used zustand provider as an ancestor.'
45+
)
46+
}
47+
return useProviderStore(
48+
selector as StateSelector<TState, StateSlice>,
49+
equalityFn
50+
)
51+
}
52+
53+
return {
54+
Provider,
55+
useStore,
56+
}
57+
}
58+
59+
export default createContext

tests/context.test.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react'
2+
import { cleanup, render } from '@testing-library/react'
3+
import create from '../src/index'
4+
import createContext from '../src/context'
5+
6+
const consoleError = console.error
7+
afterEach(() => {
8+
cleanup()
9+
console.error = consoleError
10+
})
11+
12+
type CounterState = {
13+
count: number
14+
inc: () => void
15+
}
16+
17+
it('creates and uses context store', async () => {
18+
const { Provider, useStore } = createContext<CounterState>()
19+
20+
const store = create<CounterState>((set) => ({
21+
count: 0,
22+
inc: () => set((state) => ({ count: state.count + 1 })),
23+
}))
24+
25+
function Counter() {
26+
const { count, inc } = useStore()
27+
React.useEffect(inc, [inc])
28+
return <div>count: {count * 1}</div>
29+
}
30+
31+
const { findByText } = render(
32+
<Provider initialStore={store}>
33+
<Counter />
34+
</Provider>
35+
)
36+
37+
await findByText('count: 1')
38+
})
39+
40+
it('uses context store with selectors', async () => {
41+
const { Provider, useStore } = createContext<CounterState>()
42+
43+
const store = create<CounterState>((set) => ({
44+
count: 0,
45+
inc: () => set((state) => ({ count: state.count + 1 })),
46+
}))
47+
48+
function Counter() {
49+
const count = useStore((state) => state.count)
50+
const inc = useStore((state) => state.inc)
51+
React.useEffect(inc, [inc])
52+
return <div>count: {count * 1}</div>
53+
}
54+
55+
const { findByText } = render(
56+
<Provider initialStore={store}>
57+
<Counter />
58+
</Provider>
59+
)
60+
61+
await findByText('count: 1')
62+
})
63+
64+
it('throws error when not using provider', async () => {
65+
console.error = jest.fn()
66+
67+
class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
68+
constructor(props: {}) {
69+
super(props)
70+
this.state = { hasError: false }
71+
}
72+
static getDerivedStateFromError() {
73+
return { hasError: true }
74+
}
75+
render() {
76+
return this.state.hasError ? <div>errored</div> : this.props.children
77+
}
78+
}
79+
80+
const { useStore } = createContext<CounterState>()
81+
function Component() {
82+
useStore()
83+
return <div>no error</div>
84+
}
85+
86+
const { findByText } = render(
87+
<ErrorBoundary>
88+
<Component />
89+
</ErrorBoundary>
90+
)
91+
await findByText('errored')
92+
})

tsconfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
"jsx": "preserve",
66
"allowSyntheticDefaultImports": true,
77
"esModuleInterop": true,
8-
"moduleResolution": "node"
8+
"moduleResolution": "node",
9+
"baseUrl": ".",
10+
"paths": {
11+
"zustand": [
12+
"./src/index.ts"
13+
]
14+
}
915
},
1016
"include": [
1117
"src/**/*",

0 commit comments

Comments
 (0)