Skip to content

Feat(example): Add with-zustand example #17835

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

Merged
merged 4 commits into from
Nov 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions examples/with-zustand/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
33 changes: 33 additions & 0 deletions examples/with-zustand/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Zustand example

This example shows how to integrate Zustand in Next.js.

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.

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).

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.

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`.

All components have access to the Zustand store using `useStore`.

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.

## Deploy your own

Deploy the example using [Vercel](https://vercel.com):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-zustand)

## How to use

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:

```bash
npx create-next-app --example with-zustand with-zustand-app
# or
yarn create next-app --example with-zustand with-zustand-app
```

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)).
38 changes: 38 additions & 0 deletions examples/with-zustand/components/clock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useStore } from '../lib/zustandProvider'
import shallow from 'zustand/shallow'

const useClock = () => {
return useStore(
(store) => ({ lastUpdate: store.lastUpdate, light: store.light }),
shallow
)
}

const formatTime = (time) => {
// cut off except hh:mm:ss
return new Date(time).toJSON().slice(11, 19)
}

const Clock = () => {
const { lastUpdate, light } = useClock()
return (
<div className={light ? 'light' : ''}>
{formatTime(lastUpdate)}
<style jsx>{`
div {
padding: 15px;
display: inline-block;
color: #82fa58;
font: 50px menlo, monaco, monospace;
background-color: #000;
}

.light {
background-color: #999;
}
`}</style>
</div>
)
}

export default Clock
31 changes: 31 additions & 0 deletions examples/with-zustand/components/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useStore } from '../lib/zustandProvider'
import shallow from 'zustand/shallow'
const useCounter = () => {
const { count, increment, decrement, reset } = useStore(
(store) => ({
count: store.count,
increment: store.increment,
decrement: store.decrement,
reset: store.reset,
}),
shallow
)

return { count, increment, decrement, reset }
}

const Counter = () => {
const { count, increment, decrement, reset } = useCounter()
return (
<div>
<h1>
Count: <span>{count}</span>
</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}

export default Counter
26 changes: 26 additions & 0 deletions examples/with-zustand/components/nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link'

const Nav = () => {
return (
<nav>
<Link href="/">
<a>Index</a>
</Link>
<Link href="/ssg">
<a>SSG</a>
</Link>
<Link href="/ssr">
<a>SSR</a>
</Link>
<style jsx>
{`
a {
margin-right: 25px;
}
`}
</style>
</nav>
)
}

export default Nav
22 changes: 22 additions & 0 deletions examples/with-zustand/components/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import useInterval from '../lib/useInterval'
import Clock from './clock'
import Counter from './counter'
import Nav from './nav'
import { useStore } from '../lib/zustandProvider'

export default function Page() {
const { tick } = useStore()

// Tick the time every second
useInterval(() => {
tick(Date.now(), true)
}, 1000)

return (
<>
<Nav />
<Clock />
<Counter />
</>
)
}
67 changes: 67 additions & 0 deletions examples/with-zustand/lib/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useMemo } from 'react'
import create from 'zustand'

let store

const initialState = {
lastUpdate: 0,
light: false,
count: 0,
}

function initStore(preloadedState = initialState) {
return create((set, get) => ({
...initialState,
...preloadedState,
tick: (lastUpdate, light) => {
set({
lastUpdate,
light: !!light,
})
},
increment: () => {
set({
count: get().count + 1,
})
},
decrement: () => {
set({
count: get().count - 1,
})
},
reset: () => {
set({
count: initialState.count,
})
},
}))
}

export const initializeStore = (preloadedState) => {
let _store = store ?? initStore(preloadedState)

// After navigating to a page with an initial Zustand state, merge that state
// with the current state in the store, and create a new store
if (preloadedState && store) {
_store = initStore({
...store.getState(),
...preloadedState,
})
// Reset the current store
store = undefined
}

// For SSG and SSR always create a new store
if (typeof window === 'undefined') return _store
// Create the store once in the client
if (!store) store = _store

return _store
}

export function useHydrate(initialState) {
const state =
typeof initialState === 'string' ? JSON.parse(initialState) : initialState
const store = useMemo(() => initializeStore(state), [state])
return store
}
19 changes: 19 additions & 0 deletions examples/with-zustand/lib/useInterval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useRef } from 'react'

// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
const handler = (...args) => savedCallback.current(...args)

if (delay !== null) {
const id = setInterval(handler, delay)
return () => clearInterval(id)
}
}, [delay])
}

export default useInterval
14 changes: 14 additions & 0 deletions examples/with-zustand/lib/zustandProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext, useContext } from 'react'

export const StoreContext = createContext(null)

export const StoreProvider = ({ children, store }) => {
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
}

export const useStore = (selector, eqFn) => {
const store = useContext(StoreContext)
const values = store(selector, eqFn)

return values
}
16 changes: 16 additions & 0 deletions examples/with-zustand/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "with-zustand",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"zustand": "3.1.3"
},
"license": "MIT"
}
12 changes: 12 additions & 0 deletions examples/with-zustand/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { StoreProvider } from '../lib/zustandProvider'
import { useHydrate } from '../lib/store'

export default function App({ Component, pageProps }) {
const store = useHydrate(pageProps.initialZustandState)

return (
<StoreProvider store={store}>
<Component {...pageProps} />
</StoreProvider>
)
}
5 changes: 5 additions & 0 deletions examples/with-zustand/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Page from '../components/page'

export default function Index() {
return <Page />
}
20 changes: 20 additions & 0 deletions examples/with-zustand/pages/ssg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Page from '../components/page'

export default function SSG() {
return <Page />
}

// If you build and start the app, the date returned here will have the same
// value for all requests, as this method gets executed at build time.
export function getStaticProps() {
// Note that in this case we're returning the state directly, without creating
// the store first (like in /pages/ssr.js), this approach can be better and easier
return {
props: {
initialZustandState: {
lastUpdate: Date.now(),
light: false,
},
},
}
}
19 changes: 19 additions & 0 deletions examples/with-zustand/pages/ssr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Page from '../components/page'
import { initializeStore } from '../store'

export default function SSR() {
return <Page />
}

// The date returned here will be different for every request that hits the page,
// that is because the page becomes a serverless function instead of being statically
// exported when you use `getServerSideProps` or `getInitialProps`
export function getServerSideProps() {
const zustandStore = initializeStore()

zustandStore.getState().tick(Date.now(), false)

return {
props: { initialZustandState: JSON.stringify(zustandStore.getState()) },
}
}