Repository: pmndrs/zustand
Pull request #375 (2021-05-01 08:09:16)

#182 SSR data is shared

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/pmndrs/zustand/pr/375/builds/125863) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit b6d99d75af67d9eb81e624787da539e71d687d46:

|Sandbox| Source |
|--|--|
|[React](https://codesandbox.io/s/react-6wdks)| Configuration |
|[React Typescript](https://codesandbox.io/s/react-typescript-lc3oe)| Configuration |

Munawwar: Pending work & questions:

- there is a FIXME on context.ts, on useStore's type assertion. Seems like an issue with existing type for useStore?
- Update README on how to use the new context APIs
- Should we add a next.js example folder in the repo? or should we just add a link to the next.js canary repo?

I will try to add example to next.js canary repo. However I need someone to help me out in completing the documentation
dai-shi: > Should we add a next.js example folder in the repo? or should we just add a link to the next.js canary repo?

I think just adding a link is fine, assuming we update the example in the nextjs repo.
Munawwar: @dai-shi Ah, sorry.. I didn't push my changes properly last time (some last minute force push removed my new changes). Can you check again?

Patch:
diff --git a/.size-snapshot.json b/.size-snapshot.json
index 8893577..672599e 100644
--- a/.size-snapshot.json
+++ b/.size-snapshot.json
@@ -159,5 +159,19 @@
         "code": 1147
       }
     }
+  },
+  "context.js": {
+    "bundled": 814,
+    "minified": 467,
+    "gzipped": 282,
+    "treeshaked": {
+      "rollup": {
+        "code": 14,
+        "import_statements": 14
+      },
+      "webpack": {
+        "code": 998
+      }
+    }
   }
 }
diff --git a/package.json b/package.json
index 8f31eb5..be5c16e 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,11 @@
       "types": "./shallow.d.ts",
       "module": "./esm/shallow.js",
       "default": "./shallow.js"
+    },
+    "./context": {
+      "types": "./context.d.ts",
+      "module": "./esm/context.js",
+      "default": "./context.js"
     }
   },
   "sideEffects": false,
@@ -40,6 +45,7 @@
     "build:vanilla": "rollup -c --config-vanilla",
     "build:middleware": "rollup -c --config-middleware",
     "build:shallow": "rollup -c --config-shallow",
+    "build:context": "rollup -c --config-context",
     "postbuild": "yarn copy",
     "eslint": "eslint --fix 'src/**/*.{js,ts,jsx,tsx}'",
     "eslint-examples": "eslint --fix 'examples/src/**/*.{js,ts,jsx,tsx}'",
@@ -92,6 +98,13 @@
   },
   "homepage": "https://github.com/react-spring/zustand",
   "jest": {
+    "rootDir": ".",
+    "moduleNameMapper": {
+      "^zustand$": "<rootDir>/src/index.ts"
+    },
+    "modulePathIgnorePatterns": [
+      "dist"
+    ],
     "testRegex": "test.(js|ts|tsx)$",
     "coverageDirectory": "./coverage/",
     "collectCoverage": true,
diff --git a/readme.md b/readme.md
index 3892bf0..51fa654 100644
--- a/readme.md
+++ b/readme.md
@@ -410,6 +410,37 @@ const useStore = create(devtools(redux(reducer, initialState)))
 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.
 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
 
+## React context
+
+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.
+
+```jsx
+import create from 'zustand'
+import createContext from 'zustand/context'
+
+const { Provider, useStore } = createContext()
+
+const store = create(...)
+
+const App = () => (
+  <Provider initialStore={store}>
+    ...
+  </Provider>
+)
+
+const Component = () => {
+  const state = useStore()
+  const slice = useStore(selector)
+  ...
+}
+```
+
+Optionally, `createContext` accepts `initialState` to infer store types.
+
+```ts
+const { Provider, useStore } = createContext(initialState)
+```
+
 ## TypeScript
 
 ```tsx
diff --git a/src/context.ts b/src/context.ts
new file mode 100644
index 0000000..18dba3a
--- /dev/null
+++ b/src/context.ts
@@ -0,0 +1,59 @@
+import {
+  ReactNode,
+  createElement,
+  createContext as reactCreateContext,
+  useContext,
+  useRef,
+} from 'react'
+import { UseStore } from 'zustand'
+import { EqualityChecker, State, StateSelector } from './vanilla'
+
+function createContext<TState extends State>(_initialState?: TState) {
+  const ZustandContext = reactCreateContext<UseStore<TState> | undefined>(
+    undefined
+  )
+
+  const Provider = ({
+    initialStore,
+    children,
+  }: {
+    initialStore: UseStore<TState>
+    children: ReactNode
+  }) => {
+    const storeRef = useRef<UseStore<TState>>()
+
+    if (!storeRef.current) {
+      storeRef.current = initialStore
+    }
+
+    return createElement(
+      ZustandContext.Provider,
+      { value: storeRef.current },
+      children
+    )
+  }
+
+  const useStore = <StateSlice>(
+    selector?: StateSelector<TState, StateSlice>,
+    equalityFn: EqualityChecker<StateSlice> = Object.is
+  ) => {
+    // ZustandContext value is guaranteed to be stable.
+    const useProviderStore = useContext(ZustandContext)
+    if (!useProviderStore) {
+      throw new Error(
+        'Seems like you have not used zustand provider as an ancestor.'
+      )
+    }
+    return useProviderStore(
+      selector as StateSelector<TState, StateSlice>,
+      equalityFn
+    )
+  }
+
+  return {
+    Provider,
+    useStore,
+  }
+}
+
+export default createContext
diff --git a/tsconfig.json b/tsconfig.json
index babcf1a..83b23d7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,13 @@
     "jsx": "preserve",
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
-    "moduleResolution": "node"
+    "moduleResolution": "node",
+    "baseUrl": ".",
+    "paths": {
+      "zustand": [
+        "./src/index.ts"
+      ]
+    }
   },
   "include": [
     "src/**/*",


Test patch:
diff --git a/tests/context.test.tsx b/tests/context.test.tsx
new file mode 100644
index 0000000..12ec52a
--- /dev/null
+++ b/tests/context.test.tsx
@@ -0,0 +1,92 @@
+import React from 'react'
+import { cleanup, render } from '@testing-library/react'
+import create from '../src/index'
+import createContext from '../src/context'
+
+const consoleError = console.error
+afterEach(() => {
+  cleanup()
+  console.error = consoleError
+})
+
+type CounterState = {
+  count: number
+  inc: () => void
+}
+
+it('creates and uses context store', async () => {
+  const { Provider, useStore } = createContext<CounterState>()
+
+  const store = create<CounterState>((set) => ({
+    count: 0,
+    inc: () => set((state) => ({ count: state.count + 1 })),
+  }))
+
+  function Counter() {
+    const { count, inc } = useStore()
+    React.useEffect(inc, [inc])
+    return <div>count: {count * 1}</div>
+  }
+
+  const { findByText } = render(
+    <Provider initialStore={store}>
+      <Counter />
+    </Provider>
+  )
+
+  await findByText('count: 1')
+})
+
+it('uses context store with selectors', async () => {
+  const { Provider, useStore } = createContext<CounterState>()
+
+  const store = create<CounterState>((set) => ({
+    count: 0,
+    inc: () => set((state) => ({ count: state.count + 1 })),
+  }))
+
+  function Counter() {
+    const count = useStore((state) => state.count)
+    const inc = useStore((state) => state.inc)
+    React.useEffect(inc, [inc])
+    return <div>count: {count * 1}</div>
+  }
+
+  const { findByText } = render(
+    <Provider initialStore={store}>
+      <Counter />
+    </Provider>
+  )
+
+  await findByText('count: 1')
+})
+
+it('throws error when not using provider', async () => {
+  console.error = jest.fn()
+
+  class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
+    constructor(props: {}) {
+      super(props)
+      this.state = { hasError: false }
+    }
+    static getDerivedStateFromError() {
+      return { hasError: true }
+    }
+    render() {
+      return this.state.hasError ? <div>errored</div> : this.props.children
+    }
+  }
+
+  const { useStore } = createContext<CounterState>()
+  function Component() {
+    useStore()
+    return <div>no error</div>
+  }
+
+  const { findByText } = render(
+    <ErrorBoundary>
+      <Component />
+    </ErrorBoundary>
+  )
+  await findByText('errored')
+})