Skip to content

Commit f300eca

Browse files
authored
Feat(example): Add with-zustand example (#17835)
Helps with pmndrs/zustand#182 I have seen the comments from this PR #11222 (review) and this PR matches the with-redux example.
1 parent b8c49ae commit f300eca

File tree

14 files changed

+356
-0
lines changed

14 files changed

+356
-0
lines changed

examples/with-zustand/.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env.local
29+
.env.development.local
30+
.env.test.local
31+
.env.production.local
32+
33+
# vercel
34+
.vercel

examples/with-zustand/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Zustand example
2+
3+
This example shows how to integrate Zustand in Next.js.
4+
5+
Usually splitting your app state into `pages` feels natural but sometimes you'll want to have global state for your app. This is an example on how you can use Zustand that also works with Next.js's universal rendering approach.
6+
7+
In the first example we are going to display a digital clock that updates every second. The first render is happening in the server and then the browser will take over. To illustrate this, the server rendered clock will have a different background color (black) than the client one (grey).
8+
9+
To illustrate SSG and SSR, go to `/ssg` and `/ssr`, those pages are using Next.js data fetching methods to get the date in the server and return it as props to the page, and then the browser will hydrate the store and continue updating the date.
10+
11+
The trick here for supporting universal Zustand is to separate the cases for the client and the server. When we are on the server we want to create a new store every time, otherwise different users data will be mixed up. If we are in the client we want to use always the same store. That's what we accomplish on `store.js`.
12+
13+
All components have access to the Zustand store using `useStore`.
14+
15+
On the server side every request initializes a new store, because otherwise different user data can be mixed up. On the client side the same store is used, even between page changes.
16+
17+
## Deploy your own
18+
19+
Deploy the example using [Vercel](https://vercel.com):
20+
21+
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-zustand)
22+
23+
## How to use
24+
25+
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
26+
27+
```bash
28+
npx create-next-app --example with-zustand with-zustand-app
29+
# or
30+
yarn create next-app --example with-zustand with-zustand-app
31+
```
32+
33+
Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useStore } from '../lib/zustandProvider'
2+
import shallow from 'zustand/shallow'
3+
4+
const useClock = () => {
5+
return useStore(
6+
(store) => ({ lastUpdate: store.lastUpdate, light: store.light }),
7+
shallow
8+
)
9+
}
10+
11+
const formatTime = (time) => {
12+
// cut off except hh:mm:ss
13+
return new Date(time).toJSON().slice(11, 19)
14+
}
15+
16+
const Clock = () => {
17+
const { lastUpdate, light } = useClock()
18+
return (
19+
<div className={light ? 'light' : ''}>
20+
{formatTime(lastUpdate)}
21+
<style jsx>{`
22+
div {
23+
padding: 15px;
24+
display: inline-block;
25+
color: #82fa58;
26+
font: 50px menlo, monaco, monospace;
27+
background-color: #000;
28+
}
29+
30+
.light {
31+
background-color: #999;
32+
}
33+
`}</style>
34+
</div>
35+
)
36+
}
37+
38+
export default Clock
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useStore } from '../lib/zustandProvider'
2+
import shallow from 'zustand/shallow'
3+
const useCounter = () => {
4+
const { count, increment, decrement, reset } = useStore(
5+
(store) => ({
6+
count: store.count,
7+
increment: store.increment,
8+
decrement: store.decrement,
9+
reset: store.reset,
10+
}),
11+
shallow
12+
)
13+
14+
return { count, increment, decrement, reset }
15+
}
16+
17+
const Counter = () => {
18+
const { count, increment, decrement, reset } = useCounter()
19+
return (
20+
<div>
21+
<h1>
22+
Count: <span>{count}</span>
23+
</h1>
24+
<button onClick={increment}>+1</button>
25+
<button onClick={decrement}>-1</button>
26+
<button onClick={reset}>Reset</button>
27+
</div>
28+
)
29+
}
30+
31+
export default Counter
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Link from 'next/link'
2+
3+
const Nav = () => {
4+
return (
5+
<nav>
6+
<Link href="/">
7+
<a>Index</a>
8+
</Link>
9+
<Link href="/ssg">
10+
<a>SSG</a>
11+
</Link>
12+
<Link href="/ssr">
13+
<a>SSR</a>
14+
</Link>
15+
<style jsx>
16+
{`
17+
a {
18+
margin-right: 25px;
19+
}
20+
`}
21+
</style>
22+
</nav>
23+
)
24+
}
25+
26+
export default Nav
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import useInterval from '../lib/useInterval'
2+
import Clock from './clock'
3+
import Counter from './counter'
4+
import Nav from './nav'
5+
import { useStore } from '../lib/zustandProvider'
6+
7+
export default function Page() {
8+
const { tick } = useStore()
9+
10+
// Tick the time every second
11+
useInterval(() => {
12+
tick(Date.now(), true)
13+
}, 1000)
14+
15+
return (
16+
<>
17+
<Nav />
18+
<Clock />
19+
<Counter />
20+
</>
21+
)
22+
}

examples/with-zustand/lib/store.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useMemo } from 'react'
2+
import create from 'zustand'
3+
4+
let store
5+
6+
const initialState = {
7+
lastUpdate: 0,
8+
light: false,
9+
count: 0,
10+
}
11+
12+
function initStore(preloadedState = initialState) {
13+
return create((set, get) => ({
14+
...initialState,
15+
...preloadedState,
16+
tick: (lastUpdate, light) => {
17+
set({
18+
lastUpdate,
19+
light: !!light,
20+
})
21+
},
22+
increment: () => {
23+
set({
24+
count: get().count + 1,
25+
})
26+
},
27+
decrement: () => {
28+
set({
29+
count: get().count - 1,
30+
})
31+
},
32+
reset: () => {
33+
set({
34+
count: initialState.count,
35+
})
36+
},
37+
}))
38+
}
39+
40+
export const initializeStore = (preloadedState) => {
41+
let _store = store ?? initStore(preloadedState)
42+
43+
// After navigating to a page with an initial Zustand state, merge that state
44+
// with the current state in the store, and create a new store
45+
if (preloadedState && store) {
46+
_store = initStore({
47+
...store.getState(),
48+
...preloadedState,
49+
})
50+
// Reset the current store
51+
store = undefined
52+
}
53+
54+
// For SSG and SSR always create a new store
55+
if (typeof window === 'undefined') return _store
56+
// Create the store once in the client
57+
if (!store) store = _store
58+
59+
return _store
60+
}
61+
62+
export function useHydrate(initialState) {
63+
const state =
64+
typeof initialState === 'string' ? JSON.parse(initialState) : initialState
65+
const store = useMemo(() => initializeStore(state), [state])
66+
return store
67+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useEffect, useRef } from 'react'
2+
3+
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
4+
const useInterval = (callback, delay) => {
5+
const savedCallback = useRef()
6+
useEffect(() => {
7+
savedCallback.current = callback
8+
}, [callback])
9+
useEffect(() => {
10+
const handler = (...args) => savedCallback.current(...args)
11+
12+
if (delay !== null) {
13+
const id = setInterval(handler, delay)
14+
return () => clearInterval(id)
15+
}
16+
}, [delay])
17+
}
18+
19+
export default useInterval
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createContext, useContext } from 'react'
2+
3+
export const StoreContext = createContext(null)
4+
5+
export const StoreProvider = ({ children, store }) => {
6+
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
7+
}
8+
9+
export const useStore = (selector, eqFn) => {
10+
const store = useContext(StoreContext)
11+
const values = store(selector, eqFn)
12+
13+
return values
14+
}

examples/with-zustand/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "with-zustand",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"dev": "next",
6+
"build": "next build",
7+
"start": "next start"
8+
},
9+
"dependencies": {
10+
"next": "latest",
11+
"react": "^16.9.0",
12+
"react-dom": "^16.9.0",
13+
"zustand": "3.1.3"
14+
},
15+
"license": "MIT"
16+
}

examples/with-zustand/pages/_app.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { StoreProvider } from '../lib/zustandProvider'
2+
import { useHydrate } from '../lib/store'
3+
4+
export default function App({ Component, pageProps }) {
5+
const store = useHydrate(pageProps.initialZustandState)
6+
7+
return (
8+
<StoreProvider store={store}>
9+
<Component {...pageProps} />
10+
</StoreProvider>
11+
)
12+
}

examples/with-zustand/pages/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Page from '../components/page'
2+
3+
export default function Index() {
4+
return <Page />
5+
}

examples/with-zustand/pages/ssg.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Page from '../components/page'
2+
3+
export default function SSG() {
4+
return <Page />
5+
}
6+
7+
// If you build and start the app, the date returned here will have the same
8+
// value for all requests, as this method gets executed at build time.
9+
export function getStaticProps() {
10+
// Note that in this case we're returning the state directly, without creating
11+
// the store first (like in /pages/ssr.js), this approach can be better and easier
12+
return {
13+
props: {
14+
initialZustandState: {
15+
lastUpdate: Date.now(),
16+
light: false,
17+
},
18+
},
19+
}
20+
}

examples/with-zustand/pages/ssr.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Page from '../components/page'
2+
import { initializeStore } from '../store'
3+
4+
export default function SSR() {
5+
return <Page />
6+
}
7+
8+
// The date returned here will be different for every request that hits the page,
9+
// that is because the page becomes a serverless function instead of being statically
10+
// exported when you use `getServerSideProps` or `getInitialProps`
11+
export function getServerSideProps() {
12+
const zustandStore = initializeStore()
13+
14+
zustandStore.getState().tick(Date.now(), false)
15+
16+
return {
17+
props: { initialZustandState: JSON.stringify(zustandStore.getState()) },
18+
}
19+
}

0 commit comments

Comments
 (0)