updated zustand example to 4.3.6, changed out deprecated methods (#46911)

updated zustand example to 4.3.6
changed out zustand's deprecated methods (createContext, create)
converted the example to typescript

## Why
zustand's example in nextjs repo is for zustand v3 which is quite
different to how things are done in v4, it was also in javascript.
back when when I started to use zustand in my nextjs app, this example
helped me a lot and now, I wanna do the same for devs that come here to
see how they can integrate next and zustand.

## Documentation / Examples
[✓] Make sure the linting passes by running pnpm build && pnpm lint
[✓] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
Mojtaba 2023-03-13 21:26:08 -07:00 committed by GitHub
parent c3ef208282
commit 2579ad7648
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 182 additions and 161 deletions

View file

@ -6,13 +6,11 @@ Usually splitting your app state into `pages` feels natural but sometimes you'll
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.
To illustrate SSG, go to `/ssg` and to illustrate SSR go to `/`, 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`.
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 with the `initialZustandState` returned from the get\*Props methods.
All components have access to the Zustand store using `useStore()` returned from zustand's `createContext()` function.
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.
All components have access to the Zustand store using `useStore()` returned `store.ts` file.
## Deploy your own

View file

@ -1,26 +0,0 @@
import Link from 'next/link'
const Nav = () => {
return (
<nav>
<Link href="/" legacyBehavior>
<a>Index</a>
</Link>
<Link href="/ssg" legacyBehavior>
<a>SSG</a>
</Link>
<Link href="/ssr" legacyBehavior>
<a>SSR</a>
</Link>
<style jsx>
{`
a {
margin-right: 25px;
}
`}
</style>
</nav>
)
}
export default Nav

View file

@ -1,85 +0,0 @@
import { useLayoutEffect } from 'react'
import create from 'zustand'
import createContext from 'zustand/context'
let store
const getDefaultInitialState = () => ({
lastUpdate: Date.now(),
light: false,
count: 0,
})
const zustandContext = createContext()
export const Provider = zustandContext.Provider
// An example of how to get types
/** @type {import('zustand/index').UseStore<typeof initialState>} */
export const useStore = zustandContext.useStore
export const initializeStore = (preloadedState = {}) => {
return create((set, get) => ({
...getDefaultInitialState(),
...preloadedState,
tick: (lastUpdate, light) => {
set({
lastUpdate,
light: !!light,
})
},
increment: () => {
set({
count: get().count + 1,
})
},
decrement: () => {
set({
count: get().count - 1,
})
},
reset: () => {
set({
count: getDefaultInitialState().count,
})
},
}))
}
export function useCreateStore(serverInitialState) {
// Server side code: For SSR & SSG, always use a new store.
if (typeof window === 'undefined') {
return () => initializeStore(serverInitialState)
}
// End of server side code
// Client side code:
// Next.js always re-uses same store regardless of whether page is a SSR or SSG or CSR type.
const isReusingStore = Boolean(store)
store = store ?? initializeStore(serverInitialState)
// When next.js re-renders _app while re-using an older store, then replace current state with
// the new state (in the next render cycle).
// (Why next render cycle? Because react cannot re-render while a render is already in progress.
// i.e. we cannot do a setState() as that will initiate a re-render)
//
// eslint complaining "React Hooks must be called in the exact same order in every component render"
// is ignorable as this code runs in same order in a given environment (i.e. client or server)
// eslint-disable-next-line react-hooks/rules-of-hooks
useLayoutEffect(() => {
// serverInitialState is undefined for CSR pages. It is up to you if you want to reset
// states on CSR page navigation or not. I have chosen not to, but if you choose to,
// then add `serverInitialState = getDefaultInitialState()` here.
if (serverInitialState && isReusingStore) {
store.setState(
{
// re-use functions from existing store
...store.getState(),
// but reset all other properties.
...serverInitialState,
},
true // replace states, rather than shallow merging
)
}
})
return () => store
}

View file

@ -1,14 +1,19 @@
{
"private": true,
"scripts": {
"dev": "next",
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "18.14.6",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zustand": "^3.7.1"
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5",
"zustand": "^4.3.6"
}
}

View file

@ -1,10 +0,0 @@
import { useCreateStore, Provider } from '../lib/store'
export default function App({ Component, pageProps }) {
const createStore = useCreateStore(pageProps.initialZustandState)
return (
<Provider createStore={createStore}>
<Component {...pageProps} />
</Provider>
)
}

View file

@ -1,5 +0,0 @@
import Page from '../components/page'
export default function Index() {
return <Page />
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View file

@ -1,14 +1,13 @@
import { useStore } from '../lib/store'
import shallow from 'zustand/shallow'
const useClock = () => {
return useStore(
(store) => ({ lastUpdate: store.lastUpdate, light: store.light }),
shallow
)
return useStore((store) => ({
lastUpdate: store.lastUpdate,
light: store.light,
}))
}
const formatTime = (time) => {
const formatTime = (time: number) => {
// cut off except hh:mm:ss
return new Date(time).toJSON().slice(11, 19)
}

View file

@ -1,17 +1,12 @@
import { useStore } from '../lib/store'
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 useCounter = () => {
return useStore((store) => ({
count: store.count,
increment: store.increment,
decrement: store.decrement,
reset: store.reset,
}))
}
const Counter = () => {

View file

@ -0,0 +1,19 @@
import Link from 'next/link'
import { CSSProperties } from 'react'
const LinkStyle: CSSProperties = { marginRight: '25px' }
const Nav = () => {
return (
<nav>
<Link href="/ssg" style={LinkStyle}>
SSG
</Link>
<Link href="/" style={LinkStyle}>
SSR
</Link>
</nav>
)
}
export default Nav

View file

@ -5,7 +5,7 @@ import Nav from './nav'
import { useStore } from '../lib/store'
export default function Page() {
const { tick } = useStore()
const tick = useStore((store) => store.tick)
// Tick the time every second
useInterval(() => {

View file

@ -0,0 +1,15 @@
import { type PropsWithChildren, useRef } from 'react'
import type { StoreType } from './store'
import { initializeStore, Provider } from './store'
const StoreProvider = ({ children, ...props }: PropsWithChildren) => {
const storeRef = useRef<StoreType>()
if (!storeRef.current) {
storeRef.current = initializeStore(props)
}
return <Provider value={storeRef.current}>{children}</Provider>
}
export default StoreProvider

View file

@ -0,0 +1,62 @@
import { createContext, useContext } from 'react'
import { createStore, useStore as useZustandStore } from 'zustand'
interface StoreInterface {
lastUpdate: number
light: boolean
count: number
tick: (lastUpdate: number, light: boolean) => void
increment: () => void
decrement: () => void
reset: () => void
}
const getDefaultInitialState = () => ({
lastUpdate: Date.now(),
light: false,
count: 0,
})
export type StoreType = ReturnType<typeof initializeStore>
const zustandContext = createContext<StoreType | null>(null)
export const Provider = zustandContext.Provider
export const useStore = <T>(selector: (state: StoreInterface) => T) => {
const store = useContext(zustandContext)
if (!store) throw new Error('Store is missing the provider')
return useZustandStore(store, selector)
}
export const initializeStore = (
preloadedState: Partial<StoreInterface> = {}
) => {
return createStore<StoreInterface>((set, get) => ({
...getDefaultInitialState(),
...preloadedState,
tick: (lastUpdate, light) => {
set({
lastUpdate,
light: !!light,
})
},
increment: () => {
set({
count: get().count + 1,
})
},
decrement: () => {
set({
count: get().count - 1,
})
},
reset: () => {
set({
count: getDefaultInitialState().count,
})
},
}))
}

View file

@ -1,13 +1,15 @@
import { useEffect, useRef } from 'react'
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
const useInterval = (callback, delay) => {
const savedCallback = useRef()
const useInterval = (callback: () => void, delay: number | undefined) => {
const savedCallback = useRef<typeof callback>()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
const handler = (...args) => savedCallback.current(...args)
const handler = () => savedCallback.current?.()
if (delay !== null) {
const id = setInterval(handler, delay)

View file

@ -0,0 +1,10 @@
import StoreProvider from '@/lib/StoreProvider'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<StoreProvider {...pageProps.initialZustandState}>
<Component {...pageProps} />
</StoreProvider>
)
}

View file

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

View file

@ -1,15 +1,17 @@
import type { GetServerSideProps } from 'next'
import Page from '../components/page'
import { initializeStore } from '../lib/store'
export default function SSR() {
export default function Home() {
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() {
export const getServerSideProps: GetServerSideProps = async () => {
const zustandStore = initializeStore()
return {
props: {
// the "stringify and then parse again" piece is required as next.js

View file

@ -1,3 +1,4 @@
import { GetStaticProps } from 'next'
import Page from '../components/page'
import { initializeStore } from '../lib/store'
@ -8,7 +9,7 @@ export default function SSG() {
// 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.
// You will not see this while in development mode though.
export function getStaticProps() {
export const getStaticProps: GetStaticProps = () => {
const zustandStore = initializeStore()
return {
props: {

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}