diff --git a/docs/community/docs-contributors.md b/docs/community/docs-contributors.md new file mode 100644 index 0000000000..1796fddf5b --- /dev/null +++ b/docs/community/docs-contributors.md @@ -0,0 +1,5 @@ +--- +title: Docs Contributors +description: +nav: 301 +--- diff --git a/docs/community/third-party-libraries.md b/docs/community/third-party-libraries.md new file mode 100644 index 0000000000..cd9da9a96f --- /dev/null +++ b/docs/community/third-party-libraries.md @@ -0,0 +1,64 @@ +--- +title: Third-party Libraries +description: +nav: 302 +--- + +Zustand provides bear necessities for state management. +Although it is great for most projects, +some users wish to extend the library's feature set. +This can be done using third-party libraries created by the community. + +> [!IMPORTANT] +> These libraries may have bugs, limited maintenance, +> or other limitations, and are not officially recommended +> by pmndrs or the Zustand maintainers. +> This list aims to provide a good starting point +> for someone looking to extend Zustand's feature set. + +- [@colorfy-software/zfy](https://colorfy-software.gitbook.io/zfy/) — 🧸 Useful helpers for state management in React with Zustand. +- [@davstack/store](https://www.npmjs.com/package/@davstack/store) — A zustand store factory that auto generates selectors with get/set/use methods, supports inferred types, and makes global / local state management easy. +- [@dhmk/zustand-lens](https://github.com/dhmk083/dhmk-zustand-lens) — Lens support for Zustand. +- [@liveblocks/zustand](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-zustand) — Liveblocks middleware to make your application multiplayer. +- [@prncss-xyz/zustand-optics](https://github.com/prncss-xyz/zustand-optics) — An adapter for [optics-ts](https://github.com/akheron/optics-ts). +- [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) — Automatic generation of Zustand hooks with Typescript support. +- [derive-zustand](https://github.com/zustandjs/derive-zustand) — A function to create a derived Zustand store from other Zustand stores. +- [geschichte](https://github.com/BowlingX/geschichte) — Zustand and Immer-based hook to manage query parameters. +- [leiten-zustand](https://github.com/hecmatyar/leiten-zustand) — Cleans your store from boilerplate for requests and data transformation. +- [mobz](https://github.com/2A5F/Mobz) — Zustand-style MobX API. +- [ngx-zustand](https://github.com/JoaoPauloLousada/ngx-zustand) - A Zustand adapter for Angular. +- [persist-and-sync](https://github.com/mayank1513/persist-and-sync) - Zustand middleware to easily persist and sync Zustand state between tabs/windows/iframes with same origin. +- [shared-zustand](https://github.com/Tom-Julux/shared-zustand) — Cross-tab state sharing for Zustand. +- [simple-zustand-devtools](https://github.com/beerose/simple-zustand-devtools) — 🐻⚛️ Inspect your Zustand store in React DevTools. +- [solid-zustand](https://github.com/wobsoriano/solid-zustand) — State management in Solid using Zustand. +- [treeshakable](https://github.com/react18-tools/treeshakable) — A wrapper for library creators to avoid redundant store creation. +- [use-broadcast-ts](https://github.com/Romainlg29/use-broadcast) — Zustand middleware to share state between tabs. +- [use-zustand](https://github.com/zustandjs/use-zustand) — Another custom hook to use Zustand vanilla store. +- [vue-zustand](https://github.com/wobsoriano/vue-zustand) — State management solution for Vue based on Zustand. +- [zoov](https://github.com/InfiniteXyy/zoov) — State management solution based on Zustand with Module-like API. +- [zukeeper](https://github.com/oslabs-beta/Zukeeper) - Native devtools with state and action tracking, diffing, tree display, and time travel +- [zundo](https://github.com/charkour/zundo) — 🍜 Undo and redo middleware for Zustand, enabling time-travel in your apps. +- [zustand-ards](https://github.com/ivoilic/zustand-ards) - 💁 Simple opinionated utilities for example alternative selector formats and default shallow hooks +- [zustand-computed](https://github.com/chrisvander/zustand-computed) — A Zustand middleware to create computed states. +- [zustand-computed-state](https://github.com/yasintz/zustand-computed-state) — Simple middleware to add computed states. +- [zustand-constate](https://github.com/ntvinhit/zustand-constate) — Context-based state management based on Zustand and taking ideas from Constate. +- [zustand-context](https://github.com/fredericoo/zustand-context) — Create a zustand store in React Context, containing an initial value, or use it in your components with isolated, mockable instances. +- [zustand-di](https://github.com/charkour/zustand-di) - use react props to init zustand stores +- [zustand-forms](https://github.com/Conduct/zustand-forms) — Fast, type safe form states as Zustand stores. +- [zustand-interval-persist](https://www.npmjs.com/package/zustand-interval-persist) — An enhancement for zustand that enables automatic saving of the store's state to the specified storage at regular interval. +- [zustand-middleware-computed-state](https://github.com/cmlarsen/zustand-middleware-computed-state) — A dead simple middleware for adding computed state to Zustand. +- [zustand-middleware-xstate](https://github.com/biowaffeln/zustand-middleware-xstate) — A middleware for putting XState state machines into a global Zustand store. +- [zustand-middleware-yjs](https://github.com/joebobmiles/zustand-middleware-yjs) — A middleware for synchronizing Zustand stores with Yjs. +- [zustand-persist](https://github.com/roadmanfong/zustand-persist) — A middleware for persisting and rehydrating state. +- [zustand-pub](https://github.com/AwesomeDevin/zustand-pub) - Cross-Application/Cross-Framework State Management And Sharing based on zustand and zustand-vue for React/Vue. +- [zustand-querystring](https://github.com/nitedani/zustand-querystring) — A Zustand middleware that syncs the store with the querystring. +- [zustand-rx](https://github.com/patdx/zustand-rx) — A Zustand middleware enabling you to subscribe to a store as an RxJS Observable. +- [zustand-saga](https://github.com/Nowsta/zustand-saga) — A Zustand middleware for redux-saga (minus redux). +- [zustand-slices](https://github.com/zustandjs/zustand-slices) — A slice utility for Zustand. +- [zustand-store-addons](https://github.com/Diablow/zustand-store-addons) — React state management addons for Zustand. +- [zustand-sync-tabs](https://github.com/mayank1513/zustand-sync-tabs) — Zustand middleware to easily sync Zustand state between tabs/windows/iframes with same origin. +- [zustand-vue](https://github.com/AwesomeDevin/zustand-vue) - State management for vue (Vue3 / Vue2) based on zustand. +- [zustand-yjs](https://github.com/tandem-pt/zustand-yjs) — Zustand stores for Yjs structures. +- [zusteller](https://github.com/timkindberg/zusteller) — Your global state savior. "Just hooks" + Zustand. +- [zustood](https://github.com/udecode/zustood) — 🐻‍❄️ A modular store factory using Zustand. +- [zusty](https://github.com/oslabs-beta/Zusty) - Zustand tool to assist debugging with time travel, action logs, state snapshots, store view, render time metrics and state component tree. diff --git a/docs/community/versioning-policy.md b/docs/community/versioning-policy.md new file mode 100644 index 0000000000..235b12443f --- /dev/null +++ b/docs/community/versioning-policy.md @@ -0,0 +1,5 @@ +--- +title: Versioning Policy +description: +nav: 303 +--- diff --git a/docs/community/zustand-videos.md b/docs/community/zustand-videos.md new file mode 100644 index 0000000000..fc6ca38290 --- /dev/null +++ b/docs/community/zustand-videos.md @@ -0,0 +1,5 @@ +--- +title: Zustand Videos +description: +nav: 300 +--- diff --git a/docs/learn/async-data-and-data-fetching.md b/docs/learn/async-data-and-data-fetching.md new file mode 100644 index 0000000000..961d31cb1f --- /dev/null +++ b/docs/learn/async-data-and-data-fetching.md @@ -0,0 +1,5 @@ +--- +title: Async Data and Data Fetching +description: +nav: 106 +--- diff --git a/docs/learn/choosing-the-state-structure.md b/docs/learn/choosing-the-state-structure.md new file mode 100644 index 0000000000..4fdc80941c --- /dev/null +++ b/docs/learn/choosing-the-state-structure.md @@ -0,0 +1,5 @@ +--- +title: Choosing the State Structure +description: One store, multiple stores, and multiple slices +nav: 104 +--- diff --git a/docs/learn/colocation-actions.md b/docs/learn/colocation-actions.md new file mode 100644 index 0000000000..8709eb6d10 --- /dev/null +++ b/docs/learn/colocation-actions.md @@ -0,0 +1,5 @@ +--- +title: Colocating Actions +description: +nav: 105 +--- diff --git a/docs/learn/extracting-state-logic-into-a-reducer-with-redux-and-devtools-middlewares.md b/docs/learn/extracting-state-logic-into-a-reducer-with-redux-and-devtools-middlewares.md new file mode 100644 index 0000000000..7c6040de9d --- /dev/null +++ b/docs/learn/extracting-state-logic-into-a-reducer-with-redux-and-devtools-middlewares.md @@ -0,0 +1,5 @@ +--- +title: Extracting State Logic into a Reducer with Redux and Devtools Middlewares +description: +nav: 112 +--- diff --git a/docs/learn/isolating-state-between-components.md b/docs/learn/isolating-state-between-components.md new file mode 100644 index 0000000000..a60565507a --- /dev/null +++ b/docs/learn/isolating-state-between-components.md @@ -0,0 +1,5 @@ +--- +title: Isolating State Between Components +description: +nav: 108 +--- diff --git a/docs/learn/listening-to-selected-data-with-subscribe-with-selector-middleware.md b/docs/learn/listening-to-selected-data-with-subscribe-with-selector-middleware.md new file mode 100644 index 0000000000..e331b95fb5 --- /dev/null +++ b/docs/learn/listening-to-selected-data-with-subscribe-with-selector-middleware.md @@ -0,0 +1,5 @@ +--- +title: Listening to Selected Data with SubscribeWithSelector Middleware +description: +nav: 113 +--- diff --git a/docs/learn/middleware-use-cases.md b/docs/learn/middleware-use-cases.md new file mode 100644 index 0000000000..9c56b266d6 --- /dev/null +++ b/docs/learn/middleware-use-cases.md @@ -0,0 +1,5 @@ +--- +title: Middleware Use Cases +description: +nav: 114 +--- diff --git a/docs/learn/nextjs.md b/docs/learn/nextjs.md new file mode 100644 index 0000000000..7dd2f89d5d --- /dev/null +++ b/docs/learn/nextjs.md @@ -0,0 +1,412 @@ +--- +title: Next.js +description: Setup with Next.js +nav: 116 +--- + +# Next.js + +## Setup with Next.js + +[Next.js](https://nextjs.org) is a popular server-side rendering framework for React that presents +some unique challenges for using Zustand properly. +Keep in mind that Zustand store is a global +variable (AKA module state) making it optional to use a `Context`. +These challenges include: + +- **Per-request store:** A Next.js server can handle multiple requests simultaneously. This means + that the store should be created per request and should not be shared across requests. +- **SSR friendly:** Next.js applications are rendered twice, first on the server + and again on the client. Having different outputs on both the client and the server will result + in "hydration errors." The store will have to be initialized on the server and then + re-initialized on the client with the same data in order to avoid that. Please read more about + that in our [SSR and Hydration](./ssr-and-hydration) guide. +- **SPA routing friendly:** Next.js supports a hybrid model for client side routing, which means + that in order to reset a store, we need to initialize it at the component level using a + `Context`. +- **Server caching friendly:** Recent versions of Next.js (specifically applications using the App + Router architecture) support aggressive server caching. Due to our store being a **module state**, + it is completely compatible with this caching. + +We have these general recommendations for the appropriate use of Zustand: + +- **No global stores** - Because the store should not be shared across requests, it should not be defined + as a global variable. Instead, the store should be created per request. +- **React Server Components should not read from or write to the store** - RSCs cannot use hooks or + context. They aren't meant to be stateful. Having an RSC read from or write values to a global + store violates the architecture of Next.js. + +### Creating a store per request + +Let's write our store factory function that will create a new store for each +request. + +```json +// tsconfig.json +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} +``` + +> [!IMPORTANT] +> Do not forget to remove all comments from your `tsconfig.json` file. + +```ts +// src/stores/counter-store.ts +import { createStore } from 'zustand/vanilla' + +export type CounterState = { + count: number +} + +export type CounterActions = { + decrementCount: () => void + incrementCount: () => void +} + +export type CounterStore = CounterState & CounterActions + +export const defaultInitState: CounterState = { + count: 0, +} + +export const createCounterStore = ( + initState: CounterState = defaultInitState, +) => { + return createStore()((set) => ({ + ...initState, + decrementCount: () => set((state) => ({ count: state.count - 1 })), + incrementCount: () => set((state) => ({ count: state.count + 1 })), + })) +} +``` + +### Providing the store + +Let's use the `createCounterStore` in our component and share it using a context provider. + +```tsx +// src/providers/counter-store-provider.tsx +'use client' + +import { type ReactNode, createContext, useRef, useContext } from 'react' +import { useStore } from 'zustand' + +import { type CounterStore, createCounterStore } from '@/stores/counter-store' + +export type CounterStoreApi = ReturnType + +export const CounterStoreContext = createContext( + undefined, +) + +export interface CounterStoreProviderProps { + children: ReactNode +} + +export const CounterStoreProvider = ({ + children, +}: CounterStoreProviderProps) => { + const storeRef = useRef() + if (!storeRef.current) { + storeRef.current = createCounterStore() + } + + return ( + + {children} + + ) +} + +export const useCounterStore = ( + selector: (store: CounterStore) => T, +): T => { + const counterStoreContext = useContext(CounterStoreContext) + + if (!counterStoreContext) { + throw new Error(`useCounterStore must be used within CounterStoreProvider`) + } + + return useStore(counterStoreContext, selector) +} +``` + +> [!NOTE] +> In this example, we ensure that this component is re-render-safe by checking the +> value of the reference, so that the store is only created once. This component will only be +> rendered once per request on the server, but might be re-rendered multiple times on the client if +> there are stateful client components located above this component in the tree, or if this +> component also contains other mutable state that causes a re-render. + +### Initializing the store + +```ts +// src/stores/counter-store.ts +import { createStore } from 'zustand/vanilla' + +export type CounterState = { + count: number +} + +export type CounterActions = { + decrementCount: () => void + incrementCount: () => void +} + +export type CounterStore = CounterState & CounterActions + +export const initCounterStore = (): CounterState => { + return { count: new Date().getFullYear() } +} + +export const defaultInitState: CounterState = { + count: 0, +} + +export const createCounterStore = ( + initState: CounterState = defaultInitState, +) => { + return createStore()((set) => ({ + ...initState, + decrementCount: () => set((state) => ({ count: state.count - 1 })), + incrementCount: () => set((state) => ({ count: state.count + 1 })), + })) +} +``` + +```tsx +// src/providers/counter-store-provider.tsx +'use client' + +import { type ReactNode, createContext, useRef, useContext } from 'react' +import { useStore } from 'zustand' + +import { + type CounterStore, + createCounterStore, + initCounterStore, +} from '@/stores/counter-store' + +export type CounterStoreApi = ReturnType + +export const CounterStoreContext = createContext( + undefined, +) + +export interface CounterStoreProviderProps { + children: ReactNode +} + +export const CounterStoreProvider = ({ + children, +}: CounterStoreProviderProps) => { + const storeRef = useRef() + if (!storeRef.current) { + storeRef.current = createCounterStore(initCounterStore()) + } + + return ( + + {children} + + ) +} + +export const useCounterStore = ( + selector: (store: CounterStore) => T, +): T => { + const counterStoreContext = useContext(CounterStoreContext) + + if (!counterStoreContext) { + throw new Error(`useCounterStore must be used within CounterStoreProvider`) + } + + return useStore(counterStoreContext, selector) +} +``` + +### Using the store with different architectures + +There are two architectures for a Next.js application: the +[Pages Router](https://nextjs.org/docs/pages/building-your-application/routing) and the +[App Router](https://nextjs.org/docs/app/building-your-application/routing). The usage of Zustand on +both architectures should be the same with slight differences related to each architecture. + +#### Pages Router + +```tsx +// src/components/pages/home-page.tsx +import { useCounterStore } from '@/providers/counter-store-provider.ts' + +export const HomePage = () => { + const { count, incrementCount, decrementCount } = useCounterStore( + (state) => state, + ) + + return ( +
+ Count: {count} +
+ + +
+ ) +} +``` + +```tsx +// src/_app.tsx +import type { AppProps } from 'next/app' + +import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx' + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ) +} +``` + +```tsx +// src/pages/index.tsx +import { HomePage } from '@/components/pages/home-page.tsx' + +export default function Home() { + return +} +``` + +> [!IMPORTANT] +> creating a store per route would require creating and sharing the store +> at page (route) component level. Try not to use this if you do not need to create +> a store per route. + +```tsx +// src/pages/index.tsx +import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx' +import { HomePage } from '@/components/pages/home-page.tsx' + +export default function Home() { + return ( + + + + ) +} +``` + +#### App Router + +```tsx +// src/components/pages/home-page.tsx +'use client' + +import { useCounterStore } from '@/providers/counter-store-provider' + +export const HomePage = () => { + const { count, incrementCount, decrementCount } = useCounterStore( + (state) => state, + ) + + return ( +
+ Count: {count} +
+ + +
+ ) +} +``` + +```tsx +// src/app/layout.tsx +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +import { CounterStoreProvider } from '@/providers/counter-store-provider' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + ) +} +``` + +```tsx +// src/app/page.tsx +import { HomePage } from '@/components/pages/home-page' + +export default function Home() { + return +} +``` + +> [!IMPORTANT] +> Creating a store per route would require creating and sharing the store +> at page (route) component level. Try not to use this if you do not need to create +> a store per route. + +```tsx +// src/app/page.tsx +import { CounterStoreProvider } from '@/providers/counter-store-provider' +import { HomePage } from '@/components/pages/home-page' + +export default function Home() { + return ( + + + + ) +} +``` diff --git a/docs/learn/persisting-state-with-persist-middleware.md b/docs/learn/persisting-state-with-persist-middleware.md new file mode 100644 index 0000000000..d14aeb585f --- /dev/null +++ b/docs/learn/persisting-state-with-persist-middleware.md @@ -0,0 +1,5 @@ +--- +title: Persisting State with Persist Middleware +description: +nav: 111 +--- diff --git a/docs/learn/selecting-data-and-preventing-re-renders-with-selectors-and-equality-function.md b/docs/learn/selecting-data-and-preventing-re-renders-with-selectors-and-equality-function.md new file mode 100644 index 0000000000..efd54520cd --- /dev/null +++ b/docs/learn/selecting-data-and-preventing-re-renders-with-selectors-and-equality-function.md @@ -0,0 +1,5 @@ +--- +title: Selecting Data and Preventing Re-renders with selectors and equality function +description: +nav: 107 +--- diff --git a/docs/learn/ssr-and-hydration.md b/docs/learn/ssr-and-hydration.md new file mode 100644 index 0000000000..6fc3649420 --- /dev/null +++ b/docs/learn/ssr-and-hydration.md @@ -0,0 +1,205 @@ +--- +title: Server-side Rendering (SSR) and Hydration +description: +nav: 117 +--- + +# Server-side Rendering (SSR) and Hydration + +## Rendering on server and hydrating on client + +Server-side Rendering (SSR) is a technique that helps us render our components into HTML strings on +the server, send them directly to the browser, and finally "hydrate" the static markup into a fully +interactive app on the client. + +### React + +Let's say we want to render a stateless app using React. In order to do that, we need to use +`express`, `react` and `react-dom/server`. We don't need `react-dom/client` +since it's a stateless app. + +Let's dive into that: + +- `express` helps us build a web app that we can run using Node, +- `react` helps us build the UI components that we use in our app, +- `react-dom/server` helps us render our components on a server. + +```json +// tsconfig.json +{ + "compilerOptions": { + "noImplicitAny": false, + "noEmitOnError": true, + "removeComments": false, + "sourceMap": true, + "target": "esnext" + }, + "include": ["**/*"] +} +``` + +> [!IMPORTANT] +> Do not forget to remove all comments from your `tsconfig.json` file. + +```tsx +// app.tsx +export const App = () => { + return ( + + + + + Static Server-side-rendered App + + +
Hello World!
+ + + ) +} +``` + +```tsx +// server.tsx +import express from 'express' +import React from 'react' +import ReactDOMServer from 'react-dom/server' + +import { App } from './app.tsx' + +const port = Number.parseInt(process.env.PORT || '3000', 10) +const app = express() + +app.get('/', (_, res) => { + const { pipe } = ReactDOMServer.renderToPipeableStream(, { + onShellReady() { + res.setHeader('content-type', 'text/html') + pipe(res) + }, + }) +}) + +app.listen(port, () => { + console.log(`Server is listening at ${port}`) +}) +``` + +```sh +tsc --build +``` + +```sh +node server.js +``` + +## Hydration + +Hydration turns the initial HTML snapshot from the server into a fully interactive app that runs in +the browser. The right way to "hydrate" a component is by using `hydrateRoot`. + +### React + +Let's say we want to render a stateful app using React. In order to do that we need to use +`express`, `react`, `react-dom/server` and `react-dom/client`. + +Let's dive into that: + +- `express` helps us build a web app that we can run using Node, +- `react` helps us build the UI components that we use in our app, +- `react-dom/server` helps us render our components on a server, +- `react-dom/client` helps us hydrate our components on a client. + +> [!IMPORTANT] +> Do not forget that even if we can render our components on a server, it is +> important to "hydrate" them on a client to make them interactive. + +```json +// tsconfig.json +{ + "compilerOptions": { + "noImplicitAny": false, + "noEmitOnError": true, + "removeComments": false, + "sourceMap": true, + "target": "esnext" + }, + "include": ["**/*"] +} +``` + +> [!IMPORTANT] +> Do not forget to remove all comments in your `tsconfig.json` file. + +```tsx +// app.tsx +export const App = () => { + return ( + + + + + Static Server-side-rendered App + + +
Hello World!
+ + + ) +} +``` + +```tsx +// main.tsx +import ReactDOMClient from 'react-dom/client' + +import { App } from './app.tsx' + +ReactDOMClient.hydrateRoot(, document) +``` + +```tsx +// server.tsx +import express from 'express' +import React from 'react' +import ReactDOMServer from 'react-dom/server' + +import { App } from './app.tsx' + +const port = Number.parseInt(process.env.PORT || '3000', 10) +const app = express() + +app.use('/', (_, res) => { + const { pipe } = ReactDOMServer.renderToPipeableStream(, { + bootstrapScripts: ['/main.js'], + onShellReady() { + res.setHeader('content-type', 'text/html') + pipe(res) + }, + }) +}) + +app.listen(port, () => { + console.log(`Server is listening at ${port}`) +}) +``` + +```sh +tsc --build +``` + +```sh +node server.js +``` + +> [!WARNING] +> The React tree you pass to `hydrateRoot` needs to produce the same output as it did on the server. +> The most common causes leading to hydration errors include: +> +> - Extra whitespace (like newlines) around the React-generated HTML inside the root node. +> - Using checks like typeof window !== 'undefined' in your rendering logic. +> - Using browser-only APIs like `window.matchMedia` in your rendering logic. +> - Rendering different data on the server and the client. +> +> React recovers from some hydration errors, but you must fix them like other bugs. In the best case, they’ll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements. + +You can read more about the caveats and pitfalls here: [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot) diff --git a/docs/learn/start-new-project.md b/docs/learn/start-new-project.md new file mode 100644 index 0000000000..8459b38209 --- /dev/null +++ b/docs/learn/start-new-project.md @@ -0,0 +1,5 @@ +--- +title: Start a new project +description: Vanilla, React, and React Native +nav: 102 +--- diff --git a/docs/learn/testing.md b/docs/learn/testing.md new file mode 100644 index 0000000000..3100d4c084 --- /dev/null +++ b/docs/learn/testing.md @@ -0,0 +1,519 @@ +--- +title: Testing +description: Writing Tests +nav: 115 +--- + +# Testing + +## Setting Up a Test Environment + +### Test Runners + +Usually, your test runner needs to be configured to run JavaScript/TypeScript syntax. If you're +going to be testing UI components, you will likely need to configure the test runner to use JSDOM +to provide a mock DOM environment. + +See these resources for test runner configuration instructions: + +- **Jest** + - [Jest: Getting Started](https://jestjs.io/docs/getting-started) + - [Jest: Configuration - Test Environment](https://jestjs.io/docs/configuration#testenvironment-string) +- **Vitest** + - [Vitest: Getting Started](https://vitest.dev/guide) + - [Vitest: Configuration - Test Environment](https://vitest.dev/config/#environment) + +### UI and Network Testing Tools + +**We recommend using [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro) +to test out React components that connect to Zustand**. RTL is a simple and complete React DOM +testing utility that encourages good testing practices. It uses ReactDOM's `render` function and +`act` from `react-dom/tests-utils`. Futhermore, [Native Testing Library (RNTL)](https://testing-library.com/docs/react-native-testing-library/intro) +is the alternative to RTL to test out React Native components. The [Testing Library](https://testing-library.com/) +family of tools also includes adapters for many other popular frameworks. + +We also recommend using [Mock Service Worker (MSW)](https://mswjs.io/) to mock network requests, as +this means your application logic does not need to be changed or mocked when writing tests. + +- **React Testing Library (DOM)** + - [DOM Testing Library: Setup](https://testing-library.com/docs/dom-testing-library/setup) + - [React Testing Library: Setup](https://testing-library.com/docs/react-testing-library/setup) + - [Testing Library Jest-DOM Matchers](https://testing-library.com/docs/ecosystem-jest-dom) +- **Native Testing Library (React Native)** + - [Native Testing Library: Setup](https://testing-library.com/docs/react-native-testing-library/setup) +- **User Event Testing Library (DOM)** + - [User Event Testing Library: Setup](https://testing-library.com/docs/user-event/setup) +- **TypeScript for Jest** + - [TypeScript for Jest: Setup](https://kulshekhar.github.io/ts-jest/docs/getting-started/installation) +- **TypeScript for Node** + - [TypeScript for Node: Setup](https://typestrong.org/ts-node/docs/installation) +- **Mock Service Worker** + - [MSW: Installation](https://mswjs.io/docs/getting-started/install) + - [MSW: Setting up mock requests](https://mswjs.io/docs/getting-started/mocks/rest-api) + - [MSW: Mock server configuration for Node](https://mswjs.io/docs/getting-started/integrate/node) + +## Setting Up Zustand for testing + +> [!IMPORTANT] +> Since Jest and Vitest have slight differences, like Vitest using **ES modules** and Jest using +> **CommonJS modules**, you need to keep that in mind if you are using Vitest instead of Jest. + +The mock provided below will enable the relevant test runner to reset the zustand stores after each test. + +### Shared code just for testing purposes + +This shared code was added to avoid code duplication in our demo since we use the same counter store +creator for both implementations, with and without `Context` API — `createStore` and `create`, respectively. + +```ts +// shared/counter-store-creator.ts +import { type StateCreator } from 'zustand' + +export type CounterStore = { + count: number + inc: () => void +} + +export const counterStoreCreator: StateCreator = (set) => ({ + count: 1, + inc: () => set((state) => ({ count: state.count + 1 })), +}) +``` + +### Jest + +In the next steps we are going to setup our Jest environment in order to mock Zustand. + +```ts +// __mocks__/zustand.ts +import * as zustand from 'zustand' +import { act } from '@testing-library/react' + +const { create: actualCreate, createStore: actualCreateStore } = + jest.requireActual('zustand') + +// a variable to hold reset functions for all stores declared in the app +export const storeResetFns = new Set<() => void>() + +const createUncurried = (stateCreator: zustand.StateCreator) => { + const store = actualCreate(stateCreator) + const initialState = store.getInitialState() + storeResetFns.add(() => { + store.setState(initialState, true) + }) + return store +} + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const create = ((stateCreator: zustand.StateCreator) => { + console.log('zustand create mock') + + // to support curried version of create + return typeof stateCreator === 'function' + ? createUncurried(stateCreator) + : createUncurried +}) as typeof zustand.create + +const createStoreUncurried = (stateCreator: zustand.StateCreator) => { + const store = actualCreateStore(stateCreator) + const initialState = store.getInitialState() + storeResetFns.add(() => { + store.setState(initialState, true) + }) + return store +} + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const createStore = ((stateCreator: zustand.StateCreator) => { + console.log('zustand createStore mock') + + // to support curried version of createStore + return typeof stateCreator === 'function' + ? createStoreUncurried(stateCreator) + : createStoreUncurried +}) as typeof zustand.createStore + +// reset all stores after each test run +afterEach(() => { + act(() => { + storeResetFns.forEach((resetFn) => { + resetFn() + }) + }) +}) +``` + +```ts +// setup-jest.ts +import '@testing-library/jest-dom' +``` + +```ts +// jest.config.ts +import type { JestConfigWithTsJest } from 'ts-jest' + +const config: JestConfigWithTsJest = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['./setup-jest.ts'], +} + +export default config +``` + +> [!IMPORTANT] +> To use TypeScript we need to install two packages `ts-jest` and `ts-node`. + +### Vitest + +In the next steps we are going to setup our Vitest environment in order to mock Zustand. + +> **Warning:** In Vitest you can change the [root](https://vitest.dev/config/#root). +> Due to that, you need make sure that you are creating your `__mocks__` directory in the right place. +> Let's say that you change the **root** to `./src`, that means you need to create a `__mocks__` +> directory under `./src`. The end result would be `./src/__mocks__`, rather than `./__mocks__`. +> Creating `__mocks__` directory in the wrong place can lead to issues when using Vitest. + +```ts +// __mocks__/zustand.ts +import * as zustand from 'zustand' +import { act } from '@testing-library/react' + +const { create: actualCreate, createStore: actualCreateStore } = + await vi.importActual('zustand') + +// a variable to hold reset functions for all stores declared in the app +export const storeResetFns = new Set<() => void>() + +const createUncurried = (stateCreator: zustand.StateCreator) => { + const store = actualCreate(stateCreator) + const initialState = store.getInitialState() + storeResetFns.add(() => { + store.setState(initialState, true) + }) + return store +} + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const create = ((stateCreator: zustand.StateCreator) => { + console.log('zustand create mock') + + // to support curried version of create + return typeof stateCreator === 'function' + ? createUncurried(stateCreator) + : createUncurried +}) as typeof zustand.create + +const createStoreUncurried = (stateCreator: zustand.StateCreator) => { + const store = actualCreateStore(stateCreator) + const initialState = store.getInitialState() + storeResetFns.add(() => { + store.setState(initialState, true) + }) + return store +} + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const createStore = ((stateCreator: zustand.StateCreator) => { + console.log('zustand createStore mock') + + // to support curried version of createStore + return typeof stateCreator === 'function' + ? createStoreUncurried(stateCreator) + : createStoreUncurried +}) as typeof zustand.createStore + +// reset all stores after each test run +afterEach(() => { + act(() => { + storeResetFns.forEach((resetFn) => { + resetFn() + }) + }) +}) +``` + +> [!NOTE] +> Without [globals configuration](https://vitest.dev/config/#globals) enabled, we need +> to add `import { afterEach, vi } from 'vitest'` at the top. + +```ts +// global.d.ts +/// +/// +``` + +> [!NOTE] +> Without [globals configuration](https://vitest.dev/config/#globals) enabled, we do +> need to remove `/// `. + +```ts +// setup-vitest.ts +import '@testing-library/jest-dom' + +vi.mock('zustand') // to make it work like Jest (auto-mocking) +``` + +> [!NOTE] +> Without [globals configuration](https://vitest.dev/config/#globals) enabled, we need +> to add `import { vi } from 'vitest'` at the top. + +```ts +// vitest.config.ts +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./setup-vitest.ts'], + }, + }), +) +``` + +### Testing components + +In the next examples we are going to use `useCounterStore` + +> [!NOTE] +> All of these examples are written using TypeScript. + +```ts +// stores/counter-store-creator.ts +import { type StateCreator } from 'zustand' + +export type CounterStore = { + count: number + inc: () => void +} + +export const counterStoreCreator: StateCreator = (set) => ({ + count: 1, + inc: () => set((state) => ({ count: state.count + 1 })), +}) +``` + +```ts +// stores/user-counter-store.ts +import { create } from 'zustand' + +import { + type CounterStore, + counterStoreCreator, +} from '../shared/counter-store-creator' + +export const useCounterStore = create()(counterStoreCreator) +``` + +```tsx +// contexts/use-counter-store-context.tsx +import { type ReactNode, createContext, useContext, useRef } from 'react' +import { createStore } from 'zustand' +import { useStoreWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/shallow' + +import { + type CounterStore, + counterStoreCreator, +} from '../shared/counter-store-creator' + +export const createCounterStore = () => { + return createStore(counterStoreCreator) +} + +export type CounterStoreApi = ReturnType + +export const CounterStoreContext = createContext( + undefined, +) + +export interface CounterStoreProviderProps { + children: ReactNode +} + +export const CounterStoreProvider = ({ + children, +}: CounterStoreProviderProps) => { + const counterStoreRef = useRef() + if (!counterStoreRef.current) { + counterStoreRef.current = createCounterStore() + } + + return ( + + {children} + + ) +} + +export type UseCounterStoreContextSelector = (store: CounterStore) => T + +export const useCounterStoreContext = ( + selector: UseCounterStoreContextSelector, +): T => { + const counterStoreContext = useContext(CounterStoreContext) + + if (counterStoreContext === undefined) { + throw new Error( + 'useCounterStoreContext must be used within CounterStoreProvider', + ) + } + + return useStoreWithEqualityFn(counterStoreContext, selector, shallow) +} +``` + +```tsx +// components/counter/counter.tsx +import { useCounterStore } from '../../stores/use-counter-store' + +export function Counter() { + const { count, inc } = useCounterStore() + + return ( +
+

Counter Store

+

{count}

+ +
+ ) +} +``` + +```ts +// components/counter/index.ts +export * from './counter' +``` + +```tsx +// components/counter/counter.test.tsx +import { act, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { Counter } from './counter' + +describe('Counter', () => { + test('should render with initial state of 1', async () => { + renderCounter() + + expect(await screen.findByText(/^1$/)).toBeInTheDocument() + expect( + await screen.findByRole('button', { name: /one up/i }), + ).toBeInTheDocument() + }) + + test('should increase count by clicking a button', async () => { + const user = userEvent.setup() + + renderCounter() + + expect(await screen.findByText(/^1$/)).toBeInTheDocument() + + await act(async () => { + await user.click(await screen.findByRole('button', { name: /one up/i })) + }) + + expect(await screen.findByText(/^2$/)).toBeInTheDocument() + }) +}) + +const renderCounter = () => { + return render() +} +``` + +```tsx +// components/counter-with-context/counter-with-context.tsx +import { + CounterStoreProvider, + useCounterStoreContext, +} from '../../contexts/use-counter-store-context' + +const Counter = () => { + const { count, inc } = useCounterStoreContext((state) => state) + + return ( +
+

Counter Store Context

+

{count}

+ +
+ ) +} + +export const CounterWithContext = () => { + return ( + + + + ) +} +``` + +```tsx +// components/counter-with-context/index.ts +export * from './counter-with-context' +``` + +```tsx +// components/counter-with-context/counter-with-context.test.tsx +import { act, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { CounterWithContext } from './counter-with-context' + +describe('CounterWithContext', () => { + test('should render with initial state of 1', async () => { + renderCounterWithContext() + + expect(await screen.findByText(/^1$/)).toBeInTheDocument() + expect( + await screen.findByRole('button', { name: /one up/i }), + ).toBeInTheDocument() + }) + + test('should increase count by clicking a button', async () => { + const user = userEvent.setup() + + renderCounterWithContext() + + expect(await screen.findByText(/^1$/)).toBeInTheDocument() + + await act(async () => { + await user.click(await screen.findByRole('button', { name: /one up/i })) + }) + + expect(await screen.findByText(/^2$/)).toBeInTheDocument() + }) +}) + +const renderCounterWithContext = () => { + return render() +} +``` + +> [!IMPORTANT] +> Without [globals configuration](https://vitest.dev/config/#globals) enabled, we need +> to add `import { describe, test, expect } from 'vitest'` at the top of each test file. + +**CodeSandbox Demos** + +- Jest Demo: https://stackblitz.com/edit/jest-zustand +- Vitest Demo: https://stackblitz.com/edit/vitest-zustand + +## References + +- **React Testing Library**: [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro) + is a very lightweight solution for testing React components. It provides utility functions on top + of `react-dom` and `react-dom/test-utils`, in a way that encourages better testing practices. Its + primary guiding principle is: "The more your tests resemble the way your software is used, the + more confidence they can give you." +- **Native Testing Library**: [Native Testing Library (RNTL)](https://testing-library.com/docs/react-native-testing-library/intro) + is a very lightweight solution for testing React Native components, similarly to RTL, but its + functions are built on top of `react-test-renderer`. +- **Testing Implementation Details**: Blog post by Kent C. Dodds on why he recommends to avoid + [testing implementation details](https://kentcdodds.com/blog/testing-implementation-details). diff --git a/docs/learn/thinking-in-zustand.md b/docs/learn/thinking-in-zustand.md new file mode 100644 index 0000000000..19e7875280 --- /dev/null +++ b/docs/learn/thinking-in-zustand.md @@ -0,0 +1,5 @@ +--- +title: Thinking in Zustand +description: Motivation, principles, and glossary +nav: 101 +--- diff --git a/docs/learn/tutorial-tic-tac-toe.md b/docs/learn/tutorial-tic-tac-toe.md new file mode 100644 index 0000000000..3cc87ea962 --- /dev/null +++ b/docs/learn/tutorial-tic-tac-toe.md @@ -0,0 +1,1433 @@ +--- +title: 'Tutorial: Tic-Tac-Toe' +description: Building a game +nav: 100 +--- + +# Tutorial: Tic-Tac-Toe + +## Building a game + +You will build a small tic-tac-toe game during this tutorial. This tutorial does assume existing +React knowledge. The techniques you'll learn in the tutorial are fundamental to building any React +app, and fully understanding it will give you a deep understanding of React and Zustand. + +> [!NOTE] +> This tutorial is crafted for those who learn best through hands-on experience and want to swiftly +> create something tangible. It draws inspiration from React's tic-tac-toe tutorial. + +The tutorial is divided into several sections: + +- Setup for the tutorial will give you a starting point to follow the tutorial. +- Overview will teach you the fundamentals of React: components, props, and state. +- Completing the game will teach you the most common techniques in React development. +- Adding time travel will give you a deeper insight into the unique strengths of React. + +### What are you building? + +In this tutorial, you'll build an interactive tic-tac-toe game with React and Zustand. + +You can see what it will look like when you're finished here: + +```jsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine( + { + history: [Array(9).fill(null)], + currentMove: 0, + }, + (set, get) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setCurrentMove: (nextCurrentMove) => { + set((state) => ({ + currentMove: + typeof nextCurrentMove === 'function' + ? nextCurrentMove(state.currentMove) + : nextCurrentMove, + })) + }, + } + }, + ), +) + +function Square({ value, onSquareClick }) { + return ( + + ) +} + +function Board({ xIsNext, squares, onPlay }) { + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + onPlay(nextSquares) + } + + return ( + <> +
{status}
+
+ {squares.map((_, i) => ( + handleClick(i)} + /> + ))} +
+ + ) +} + +export default function Game() { + const { history, setHistory, currentMove, setCurrentMove } = useGameStore() + const xIsNext = currentMove % 2 === 0 + const currentSquares = history[currentMove] + + function handlePlay(nextSquares) { + const nextHistory = [...history.slice(0, currentMove + 1), nextSquares] + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove) + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} + +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +### Building the board + +Let's start by creating the `Square` component, which will be a building block for our `Board` +component. This component will represent each square in our game. + +The `Square` component should take `value` and `onSquareClick` as props. It should return a +` + ) +} +``` + +Let's move on to creating the Board component, which will consist of 9 squares arranged in a grid. +This component will serve as the main playing area for our game. + +The `Board` component should return a `
` element styled as a grid. The grid layout is achieved +using CSS Grid, with three columns and three rows, each taking up an equal fraction of the available +space. The overall size of the grid is determined by the width and height properties, ensuring that +it is square-shaped and appropriately sized. + +Inside the grid, we place nine Square components, each with a value prop representing its position. +These Square components will eventually hold the game symbols (`'X'` or `'O'`) and handle user +interactions. + +Here's the code for the `Board` component: + +```tsx +export default function Board() { + return ( +
+ + + + + + + + + +
+ ) +} +``` + +This Board component sets up the basic structure for our game board by arranging nine squares in a +3x3 grid. It positions the squares neatly, providing a foundation for adding more features and +handling player interactions in the future. + +### Lifting state up + +Each `Square` component could maintain a part of the game's state. To check for a winner in a +tic-tac-toe game, the `Board` component would need to somehow know the state of each of the 9 +`Square` components. + +How would you approach that? At first, you might guess that the `Board` component needs to ask each +`Square` component for that `Square`'s component state. Although this approach is technically +possible in React, we discourage it because the code becomes difficult to understand, susceptible +to bugs, and hard to refactor. Instead, the best approach is to store the game's state in the +parent `Board` component instead of in each `Square` component. The `Board` component can tell each +`Square` component what to display by passing a prop, like you did when you passed a number to each +`Square` component. + +> [!IMPORTANT] +> To collect data from multiple children, or to have two or more child components +> communicate with each other, declare the shared state in their parent component instead. The +> parent component can pass that state back down to the children via props. This keeps the child +> components in sync with each other and with their parent. + +Let's take this opportunity to try it out. Edit the `Board` component so that it declares a state +variable named squares that defaults to an array of 9 nulls corresponding to the 9 squares: + +```tsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine({ squares: Array(9).fill(null) }, (set) => { + return { + setSquares: (nextSquares) => { + set((state) => ({ + squares: + typeof nextSquares === 'function' + ? nextSquares(state.squares) + : nextSquares, + })) + }, + } + }), +) + +export default function Board() { + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + + return ( +
+ {squares.map((square, squareIndex) => ( + + ))} +
+ ) +} +``` + +`Array(9).fill(null)` creates an array with nine elements and sets each of them to `null`. The +`useSquaresStore` declares a `squares` state that's initially set to that array. Each entry in the +array corresponds to the value of a square. When you fill the board in later, the squares array +will look like this: + +```ts +const squares = ['O', null, 'X', 'X', 'X', 'O', 'O', null, null] +``` + +Each Square will now receive a `value` prop that will either be `'X'`, `'O'`, or `null` for empty +squares. + +Next, you need to change what happens when a `Square` component is clicked. The `Board` component +now maintains which squares are filled. You'll need to create a way for the `Square` component to +update the `Board`'s component state. Since state is private to a component that defines it, you +cannot update the `Board`'s component state directly from `Square` component. + +Instead, you'll pass down a function from the Board component to the `Square` component, and you'll +have `Square` component call that function when a square is clicked. You'll start with the function +that the `Square` component will call when it is clicked. You'll call that function `onSquareClick`: + +Now you'll connect the `onSquareClick` prop to a function in the `Board` component that you'll name +`handleClick`. To connect `onSquareClick` to `handleClick` you'll pass an inline function to the +`onSquareClick` prop of the first Square component: + +```tsx + handleClick(i)} /> +``` + +Lastly, you will define the `handleClick` function inside the `Board` component to update the +squares array holding your board's state. + +The `handleClick` function should take the index of the square to update and create a copy of the +`squares` array (`nextSquares`). Then, `handleClick` updates the `nextSquares` array by adding `X` +to the square at the specified index (`i`) if is not already filled. + +```tsx{7-12,29} +export default function Board() { + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + + function handleClick(i) { + if (squares[i]) return + const nextSquares = squares.slice() + nextSquares[i] = 'X' + setSquares(nextSquares) + } + + return ( +
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ ) +} +``` + +> [!IMPORTANT] +> Note how in `handleClick` function, you call `.slice()` to create a copy of the squares array +> instead of modifying the existing array. + +### Taking turns + +It's now time to fix a major defect in this tic-tac-toe game: the `'O'`s cannot be used on the +board. + +You'll set the first move to be `'X'` by default. Let's keep track of this by adding another piece +of state to the `useGameStore` hook: + +```tsx{2,12-18} +const useGameStore = create( + combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { + return { + setSquares: (nextSquares) => { + set((state) => ({ + squares: + typeof nextSquares === 'function' + ? nextSquares(state.squares) + : nextSquares, + })); + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })); + }, + }; + }) +); +``` + +Each time a player moves, `xIsNext` (a boolean) will be flipped to determine which player goes next +and the game's state will be saved. You'll update the Board's `handleClick` function to flip the +value of `xIsNext`: + +```tsx{2-5,10,15} +export default function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const player = xIsNext ? 'X' : 'O' + + function handleClick(i) { + if (squares[i]) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( +
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ ) +} +``` + +### Declaring a winner or draw + +Now that the players can take turns, you'll want to show when the game is won or drawn and there +are no more turns to make. To do this you'll add three helper functions. The first helper function +called `calculateWinner` that takes an array of 9 squares, checks for a winner and returns `'X'`, +`'O'`, or `null` as appropriate. The second helper function called `calculateTurns` that takes the +same array, checks for remaining turns by filtering out only `null` items, and returns the count of +them. The last helper called `calculateStatus` that takes the remaining turns, the winner, and the +current player (`'X' or 'O'`): + +```ts +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +You will use the result of `calculateWinner(squares)` in the Board component's `handleClick` +function to check if a player has won. You can perform this check at the same time you check if a +user has clicked a square that already has a `'X'` or and `'O'`. We'd like to return early in +both cases: + +```ts{2} +function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player' + setSquares(nextSquares) + setXIsNext(!xIsNext) +} +``` + +To let the players know when the game is over, you can display text such as `'Winner: X'` or +`'Winner: O'`. To do that you'll add a `status` section to the `Board` component. The status will +display the winner or draw if the game is over and if the game is ongoing you'll display which +player's turn is next: + +```tsx{10-11,13,25} +export default function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} +``` + +Congratulations! You now have a working tic-tac-toe game. And you've just learned the basics of +React and Zustand too. So you are the real winner here. Here is what the code should look like: + +```tsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { + return { + setSquares: (nextSquares) => { + set((state) => ({ + squares: + typeof nextSquares === 'function' + ? nextSquares(state.squares) + : nextSquares, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }), +) + +function Square({ value, onSquareClick }) { + return ( + + ) +} + +export default function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} + +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +### Adding time travel + +As a final exercise, let's make it possible to “go back in time” and revisit previous moves in the +game. + +If you had directly modified the squares array, implementing this time-travel feature would be very +difficult. However, since you used `slice()` to create a new copy of the squares array after every +move, treating it as immutable, you can store every past version of the squares array and navigate +between them. + +You'll keep track of these past squares arrays in a new state variable called `history`. This +`history` array will store all board states, from the first move to the latest one, and will look +something like this: + +```ts +const history = [ + // First move + [null, null, null, null, null, null, null, null, null], + // Second move + ['X', null, null, null, null, null, null, null, null], + // Third move + ['X', 'O', null, null, null, null, null, null, null], + // and so on... +] +``` + +This approach allows you to easily navigate between different game states and implement the +time-travel feature. + +### Lifting state up, again + +Next, you will create a new top-level component called `Game` to display a list of past moves. This +is where you will store the `history` state that contains the entire game history. + +By placing the `history` state in the `Game` component, you can remove the `squares` state from the +`Board` component. You will now lift the state up from the `Board` component to the top-level `Game` +component. This change allows the `Game` component to have full control over the `Board`'s +component data and instruct the `Board` component to render previous turns from the `history`. + +First, add a `Game` component with `export default` and remove it from `Board` component. Here is +what the code should look like: + +```tsx{1,48-65} +function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} + +export default function Game() { + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} +``` + +Add some state to the `useGameStore` hook to track the history of moves: + +```ts{2,4-11} +const useGameStore = create( + combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }), +) +``` + +Notice how `[Array(9).fill(null)]` creates an array with a single item, which is itself an array of +9 null values. + +To render the squares for the current move, you'll need to read the most recent squares array from +the `history` state. You don't need an extra state for this because you already have enough +information to calculate it during rendering: + +```tsx{2-3} +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} +``` + +Next, create a `handlePlay` function inside the `Game` component that will be called by the `Board` +component to update the game. Pass `xIsNext`, `currentSquares` and `handlePlay` as props to the +`Board` component: + +```tsx{5-7,18} +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + function handlePlay(nextSquares) { + // TODO + } + + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} +``` + +Let's make the `Board` component fully controlled by the props it receives. To do this, we'll modify +the `Board` component to accept three props: `xIsNext`, `squares`, and a new `onPlay` function that +the `Board` component can call with the updated squares array when a player makes a move. + +```tsx{1} +function Board({ xIsNext, squares, onPlay }) { + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} +``` + +The `Board` component is now fully controlled by the props passed to it by the `Game` component. To +get the game working again, you need to implement the `handlePlay` function in the `Game` +component. + +What should `handlePlay` do when called? Previously, the `Board` component called `setSquares` with +an updated array; now it passes the updated squares array to `onPlay`. + +The `handlePlay` function needs to update the `Game` component's state to trigger a re-render. +Instead of using `setSquares`, you'll update the `history` state variable by appending the updated +squares array as a new `history` entry. You also need to toggle `xIsNext`, just as the `Board` +component used +to do. + +```ts{2-3} +function handlePlay(nextSquares) { + setHistory(history.concat([nextSquares])) + setXIsNext(!xIsNext) +} +``` + +At this point, you've moved the state to live in the `Game` component, and the UI should be fully +working, just as it was before the refactor. Here is what the code should look like at this point: + +```tsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }), +) + +function Square({ value, onSquareClick }) { + return ( + + ) +} + +function Board({ xIsNext, squares, onPlay }) { + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + onPlay(nextSquares) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} + +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + function handlePlay(nextSquares) { + setHistory(history.concat([nextSquares])) + setXIsNext(!xIsNext) + } + + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} + +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +### Showing the past moves + +Since you are recording the tic-tac-toe game's history, you can now display a list of past moves to +the player. + +You already have an array of `history` moves in store, so now you need to transform it to an array +of React elements. In JavaScript, to transform one array into another, you can use the Array +`.map()` method: + +You'll use `map` to transform your `history` of moves into React elements representing buttons on the +screen, and display a list of buttons to **jump** to past moves. Let's `map` over the `history` in +the `Game` component: + +```tsx{26-41} +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + function handlePlay(nextSquares) { + setHistory(history.concat([nextSquares])) + setXIsNext(!xIsNext) + } + + function jumpTo(nextMove) { + // TODO + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} +``` + +Before you can implement the `jumpTo` function, you need the `Game` component to keep track of which +step the user is currently viewing. To do this, define a new state variable called `currentMove`, +which will start at `0`: + +```ts{3,14-21} +const useGameStore = create( + combine( + { history: [Array(9).fill(null)], currentMove: 0, xIsNext: true }, + (set) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setCurrentMove: (nextCurrentMove) => { + set((state) => ({ + currentMove: + typeof nextCurrentMove === 'function' + ? nextCurrentMove(state.currentMove) + : nextCurrentMove, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }, + ), +) +``` + +Next, update the `jumpTo` function inside `Game` component to update that `currentMove`. You’ll +also set `xIsNext` to `true` if the number that you’re changing `currentMove` to is even. + +```ts{2-3} +function jumpTo(nextMove) { + setCurrentMove(nextMove) + setXIsNext(currentMove % 2 === 0) +} +``` + +You will now make two changes to the `handlePlay` function in the `Game` component, which is called +when you click on a square. + +- If you "go back in time" and then make a new move from that point, you only want to keep the + history up to that point. Instead of adding `nextSquares` after all items in the history (using + the Array `.concat()` method), you'll add it after all items in + `history.slice(0, currentMove + 1)` to keep only that portion of the old history. +- Each time a move is made, you need to update `currentMove` to point to the latest history entry. + +```ts{2-4} +function handlePlay(nextSquares) { + const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + setXIsNext(!xIsNext) +} +``` + +Finally, you will modify the `Game` component to render the currently selected move, instead of +always rendering the final move: + +```tsx{2-10} +export default function Game() { + const { + history, + setHistory, + currentMove, + setCurrentMove, + xIsNext, + setXIsNext, + } = useGameStore() + const currentSquares = history[currentMove] + + function handlePlay(nextSquares) { + const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + setXIsNext(!xIsNext) + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove) + setXIsNext(currentMove % 2 === 0) + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} +``` + +### Final cleanup + +If you look closely at the code, you'll see that `xIsNext` is `true` when `currentMove` is even and +`false` when `currentMove` is odd. This means that if you know the value of `currentMove`, you can +always determine what `xIsNext` should be. + +There's no need to store `xIsNext` separately in the state. It’s better to avoid redundant state +because it can reduce bugs and make your code easier to understand. Instead, you can calculate +`xIsNext` based on `currentMove`: + +```tsx{2,10,14} +export default function Game() { + const { history, setHistory, currentMove, setCurrentMove } = useGameStore() + const xIsNext = currentMove % 2 === 0 + const currentSquares = history[currentMove] + + function handlePlay(nextSquares) { + const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove) + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} +``` + +You no longer need the `xIsNext` state declaration or the calls to `setXIsNext`. Now, there’s no +chance for `xIsNext` to get out of sync with `currentMove`, even if you make a mistake while coding +the components. + +### Wrapping up + +Congratulations! You’ve created a tic-tac-toe game that: + +- Lets you play tic-tac-toe, +- Indicates when a player has won the game or when is drawn, +- Stores a game’s history as a game progresses, +- Allows players to review a game’s history and see previous versions of a game’s board. + +Nice work! We hope you now feel like you have a decent grasp of how React and Zustand works. diff --git a/docs/learn/using-middlewares.md b/docs/learn/using-middlewares.md new file mode 100644 index 0000000000..b5743afd1c --- /dev/null +++ b/docs/learn/using-middlewares.md @@ -0,0 +1,5 @@ +--- +title: Using Middlewares +description: +nav: 109 +--- diff --git a/docs/learn/using-typescript.md b/docs/learn/using-typescript.md new file mode 100644 index 0000000000..6becc5232b --- /dev/null +++ b/docs/learn/using-typescript.md @@ -0,0 +1,5 @@ +--- +title: Using TypeScript +description: +nav: 103 +--- diff --git a/docs/learn/writing-immutable-update-with-immer-middleware.md b/docs/learn/writing-immutable-update-with-immer-middleware.md new file mode 100644 index 0000000000..6ea0daf4ed --- /dev/null +++ b/docs/learn/writing-immutable-update-with-immer-middleware.md @@ -0,0 +1,5 @@ +--- +title: Writing Immutable Updates with Immer Middleware +description: +nav: 110 +--- diff --git a/docs/reference/combine.md b/docs/reference/combine.md new file mode 100644 index 0000000000..e969dbaea3 --- /dev/null +++ b/docs/reference/combine.md @@ -0,0 +1,44 @@ +--- +title: combine +description: +nav: 201 +--- + +# combine + +`combine` middleware lets you create a new state by combining an initial state with additional +state created by a function. This middleware is particularly useful due to infers the state, so you +don't need to type it. + +```js +combine(initialState, additionalStateCreator) +``` + +- [Reference](#reference) + - [Signature](#combine-signature) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) + +## Reference + +### `combine` Signature + +```ts +combine(initialState: T, additionalStateCreatorFn: StateCreator): StateCreator & U, [], []> +``` + +#### Parameters + +- `initialState`: The value you want the state to be initially. It can be a value of any type, + except a function. +- `additionalStateCreatorFn`: The state creator function that specifies how the state gets + initialized and updated. It must be pure, should take `setState` function, `getState` function + and `storeApi` as arguments. + +#### Returns + +`combine` returns a state creator function. + +## Usage + +## Troubleshooting diff --git a/docs/reference/core-concepts.md b/docs/reference/core-concepts.md new file mode 100644 index 0000000000..2adb66a270 --- /dev/null +++ b/docs/reference/core-concepts.md @@ -0,0 +1,79 @@ +--- +title: Core Concepts +description: +nav: 200 +--- + +# Core Concepts + +## Store + +The Store lets you access to the current state and API utilities. + +## Store API Utilities + +These store API utilities lets you get current state, update current state and subscribe current +state. These utilities are: `setState` function, `getState` function, and `subscribe` function. + +### `setState` function + +The `setState` function lets you update the state to a different value and trigger re-render. You +can pass the next state directly, a next partial state, a function that calculates it from the +previous state, or replace it completely. + +#### Parameters + +- `nextState`: The value that you want the state to be. It can be a value of any type, but there is + a special behavior for functions. + - If you pass an object as a `nextState`. It will shallow merge `nextState` with the current + state. You can pass only the properties you want to update, this allows for selective state + updates without modifying other properties. + - If you pass a non-object as a `nextState`, make sure you use `replace` as `true` to avoid + unexpected behaviors. + - If you pass a function as a `nextState`. It must be pure, should take current state as its + only argument, and should return the next state. The next state returned by the updater + function face the same restrictions of any next state. +- `replace`: This optional boolean flag controls whether the state is completely replaced or only + shallow updated, through a shallow merge. + +#### Returns + +`setState` function do not have a return value. + +### `getState` function + +The `getState` function lets you access to the current state. It can be stale on asynchronous +operations. + +### `subscribe` function + +The `subscribe` function lets you subscribe to state updates. It should take current state, and +its previous state as arguments. + +#### Parameters + +- `currentState`: The current state. +- `previousState`: The previous state. + +#### Returns + +`subscribe` returns a function that lets you unsubscribe from itself. + +## Selector + +The selector function lets you return data that is based on current state. It should take current +state as its only argument. + +## Bound Hook + +You can create custom hooks in React that utilize closures to "bind" functionality to specific data +or contexts. Within the custom hook, you might define a function that relies on values captured in +the closure during its creation. When you use the custom hook in your component, you're essentially +getting a function that's "bound" to that specific closure's captured data (in Zustand to store API +utilities). + +## State Creator Function + +The state creator function lets you create a store, when you pass to the `create` function or +`createStore` function from Zustand. This function essentially acts as a _**blueprint**_ for your +state management needs. diff --git a/docs/reference/create-store.md b/docs/reference/create-store.md new file mode 100644 index 0000000000..77e390483f --- /dev/null +++ b/docs/reference/create-store.md @@ -0,0 +1,546 @@ +--- +title: createStore +description: +nav: 202 +--- + +# createStore + +`createStore` lets you create a vanilla store with API utilities attached. + +```js +createStore(stateCreatorFn) +``` + +- [Reference](#reference) + - [Signature](#createstore-signature) +- [Usage](#usage) + - [Updating state based on previous state](#updating-state-based-on-previous-state) + - [Updating Primitives in State](#updating-primitives-in-state) + - [Updating Objects in State](#updating-objects-in-state) + - [Updating Arrays in State](#updating-arrays-in-state) + - [Updating state with no store actions](#updating-state-with-no-store-actions) + - [Subscribing to state updates](#subscribing-to-state-updates) +- [Troubleshooting](#troubleshooting) + - [I’ve updated the state, but the screen doesn’t update](#ive-updated-the-state-but-the-screen-doesnt-update) + +## Reference + +### `createStore` Signature + +```ts +createStore()(stateCreatorFn: StateCreator): StoreApi +``` + +#### Parameters + +- `stateCreatorFn`: The value you want the state to be initially. It can be a value of any type, but + when you pass a function should take `setState` function, `getState` function and `storeApi` as + arguments. + +#### Returns + +`createStore` returns a vanilla store with some API utilities. These API utilities are: `setState` +function, `getState` function, and `subscribe` function. + +## Usage + +### Updating state based on previous state + +This example show you how you can support **updater functions** for your +**actions**. + +```tsx +import { createStore } from 'zustand' + +type AgeStoreState = { age: number } + +type AgeStoreActions = { + setAge: ( + nextAge: + | AgeStoreState['age'] + | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), + ) => void +} + +type AgeStore = AgeStoreState & AgeStoreActions + +const ageStore = createStore()((set) => ({ + age: 42, + setAge: (nextAge) => { + set((state) => ({ + age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, + })) + }, +})) + +function increment() { + ageStore.getState().setAge((currentAge) => currentAge + 1) +} + +const $yourAgeHeading = document.getElementById( + 'your-age', +) as HMTLHeadingElement +const $incrementBy3Button = document.getElementById( + 'increment-by-3', +) as HTMLButtonElement +const $incrementBy1Button = document.getElementById( + 'increment-by-1', +) as HTMLButtonElement + +$incrementBy3Button.addEventListener('click', () => { + increment() + increment() + increment() +}) + +$incrementBy1Button.addEventListener('click', () => { + increment() +}) + +const render: Parameters[0] = (state) => { + $yourAgeHeading.innerHTML = `Your age: ${state.age}` +} + +render(ageStore.getInitialState(), ageStore.getInitialState()) + +ageStore.subscribe(render) +``` + +Here's the `html` code + +```html +

+ + +``` + +### Updating Primitives in State + +State can hold any kind of JavaScript value. When you want to update built-in primitive values like +number, strings, booleans, etc. we should directly assign new values to ensure updates are applied +correctly, and avoid unexpected behaviors. + +> [!NOTE] +> By default, `set` function performs a shallow merge. If you need to completely replace +> the state with a new one, use the `replace` parameter set to `true` + +```ts +import { create } from 'zustand' + +type XStore = number + +const xStore = create()(() => 0) + +const $dotContainer = document.getElementById('dot-container') as HTMLDivElement +const $dot = document.getElementById('dot') as HTMLDivElement + +$dotContainer.addEventListener('pointermove', (event) => { + xStore.setState(event.clientX, true) +}) + +const render: Parameters[0] = (state) => { + const position = { y: 0, x: state } + + $dot.style.transform = `translate(${position.x}px, ${position.y}px)` +} + +render(xStore.getInitialState(), xStore.getInitialState()) + +xStore.subscribe(render) +``` + +Here's the `html` code + +```html +
+
+
+``` + +### Updating Objects in State + +Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store +them in state. Instead, when you want to update an object, you need to create a new one (or make a +copy of an existing one), and then set the state to use the new object. + +By default, `set` function performs a shallow merge. For most updates where you only need to modify +specific properties, the default shallow merge is preferred as it's more efficient. To completely +replace the state with a new one, use the `replace` parameter set to `true` with caution, as it +discards any existing nested data within the state. + +```ts +import { create } from 'zustand' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const positionStore = create()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) + +const $dotContainer = document.getElementById('dot-container') as HTMLDivElement +const $dot = document.getElementById('dot') as HTMLDivElement + +$dotContainer.addEventListener('pointermove', (event) => { + positionStore.getState().setPosition({ + x: event.clientX, + y: event.clientY, + }) +}) + +const render: Parameters[0] = (state) => { + const position = { x: state.x, y: state.y } + + $dot.style.transform = `translate(${position.x}px, ${position.y}px)` +} + +render(positionStore.getInitialState(), positionStore.getInitialState()) + +positionStore.subscribe(render) +``` + +Here's the `html` code + +```html +
+
+
+``` + +### Updating Arrays in State + +Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in +state. Just like with objects, when you want to update an array stored in state, you need to create +a new one (or make a copy of an existing one), and then set state to use the new array. + +By default, `set` function performs a shallow merge. To update array values we should assign new +values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely +replace the state with a new one, use the `replace` parameter set to `true`. + +> [!IMPORTANT] +> We should prefer immutable operations like: `concat(...)`, `[...array]`, `filter(...)`, +> `slice(...)`, `map(...)`, `[...array].sort(...)`, and `[...array].reverse(...)`, and avoid +> mutable operations like `push(...)`, `unshift(...)`, `pop(...)`, `shift(...)`, `splice(...)`, +> `array[arrayIndex] = ...`, `reverse(...)`, and `sort(...)`. + +```ts +import { create } from 'zustand' + +type PositionStore = [number, number] + +const positionStore = create()(() => [0, 0]) + +const $dotContainer = document.getElementById('dot-container') as HTMLDivElement +const $dot = document.getElementById('dot') as HTMLDivElement + +$dotContainer.addEventListener('pointermove', (event) => { + positionStore.setState([event.clientX, event.clientY], true) +}) + +const render: Parameters[0] = (state) => { + const position = { x: state[0], y: state[1] } + + $dot.style.transform = `translate(${position.x}px, ${position.y}px)` +} + +render(positionStore.getInitialState(), positionStore.getInitialState()) + +positionStore.subscribe(render) +``` + +Here's the `html` code + +```html +
+
+
+``` + +### Subscribing to state updates + +By subscribing to state updates, you register a callback that fires whenever the store's state +updates. We can use `subscribe` for external state management. + +```ts +import { useEffect } from 'react' +import { createStore } from 'zustand' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nextPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const positionStore = createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) + +const $dot = document.getElementById('dot') as HTMLDivElement + +$dot.addEventListener('mouseenter', (event) => { + const parent = event.currentTarget.parentElement + const parentWidth = parent.clientWidth + const parentHeight = parent.clientHeight + + positionStore.getState().setPosition({ + x: Math.ceil(Math.random() * parentWidth), + y: Math.ceil(Math.random() * parentHeight), + }) +}) + +const render: Parameters[0] = (state) => { + const position = { x: state.x, y: state.y } + + $dot.style.transform = `translate(${position.x}px, ${position.y}px)` +} + +render(positionStore.getInitialState(), positionStore.getInitialState()) + +positionStore.subscribe(render) + +const logger: Parameters[0] = (state) => { + console.log('new position', { position: { x: state.x, y: state.x } }) +} + +positionStore.subscribe(logger) +``` + +Here's the `html` code + +```html +
+
+
+``` + +## Troubleshooting + +### I’ve updated the state, but the screen doesn’t update + +In the previous example, the `position` object is always created fresh from the current cursor +position. But often, you will want to include existing data as a part of the new object you’re +creating. For example, you may want to update only one field in a form, but keep the previous +values for all other fields. + +These input fields don’t work because the `oninput` handlers mutate the state: + +```ts +import { createStore } from 'zustand' + +type PersonStoreState = { + firstName: string + lastName: string + email: string +} + +type PersonStoreActions = { + setPerson: (nextPerson: Partial) => void +} + +type PersonStore = PersonStoreState & PersonStoreActions + +const personStore = createStore()((set) => ({ + firstName: 'Barbara', + lastName: 'Hepworth', + email: 'bhepworth@sculpture.com', + setPerson: (nextPerson) => { + set(nextPerson) + }, +})) + +const $firstNameInput = document.getElementById( + 'first-name', +) as HTMLInputElement +const $lastNameInput = document.getElementById('last-name') as HTMLInputElement +const $emailInput = document.getElementById('email') as HTMLInputElement +const $result = document.getElementById('result') as HTMLDivElement + +function handleFirstNameChange(event: Event) { + personStore.getState().firstName = (event.target as any).value +} + +function handleLastNameChange(event: Event) { + personStore.getState().lastName = (event.target as any).value +} + +function handleEmailChange(event: Event) { + personStore.getState().email = (event.target as any).value +} + +$firstNameInput.addEventListener('input', handleFirstNameChange) +$lastNameInput.addEventListener('input', handleLastNameChange) +$emailInput.addEventListener('input', handleEmailChange) + +const render: Parameters[0] = (state) => { + const person = { + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + } + + $firstNameInput.value = person.firstName + $lastNameInput.value = person.lastName + $emailInput.value = person.email + + $result.innerHTML = `${person.firstName} ${person.lastName} (${person.email})` +} + +render(personStore.getInitialState(), personStore.getInitialState()) + +personStore.subscribe(render) +``` + +Here's the `html` code + +```html + + + +

+``` + +For example, this line mutates the state from a past render: + +```ts +personStore.getState().firstName = (e.target as any).value +``` + +The reliable way to get the behavior you’re looking for is to create a new object and pass it to +`setPerson`. But here, you want to also copy the existing data into it because only one of the +fields has changed: + +```ts +personStore.getState().setPerson({ + firstName: e.target.value, // New first name from the input +}) +``` + +> [!NOTE] +> We don’t need to copy every property separately due to `set` function performs shallow +> merge by default. + +Now the form works! + +Notice how you didn’t declare a separate state variable for each input field. For large forms, +keeping all data grouped in an object is very convenient—as long as you update it correctly! + +```ts +import { createStore } from 'zustand' + +type PersonStoreState = { + firstName: string + lastName: string + email: string +} + +type PersonStoreActions = { + setPerson: (nextPerson: Partial) => void +} + +type PersonStore = PersonStoreState & PersonStoreActions + +const personStore = createStore()((set) => ({ + firstName: 'Barbara', + lastName: 'Hepworth', + email: 'bhepworth@sculpture.com', + setPerson: (nextPerson) => { + set(nextPerson) + }, +})) + +const $firstNameInput = document.getElementById( + 'first-name', +) as HTMLInputElement +const $lastNameInput = document.getElementById('last-name') as HTMLInputElement +const $emailInput = document.getElementById('email') as HTMLInputElement +const $result = document.getElementById('result') as HTMLDivElement + +function handleFirstNameChange(event: Event) { + personStore.getState().setPerson({ + firstName: (event.target as any).value, + }) +} + +function handleLastNameChange(event: Event) { + personStore.getState().setPerson({ + lastName: (event.target as any).value, + }) +} + +function handleEmailChange(event: Event) { + personStore.getState().setPerson({ + email: (event.target as any).value, + }) +} + +$firstNameInput.addEventListener('input', handleFirstNameChange) +$lastNameInput.addEventListener('input', handleLastNameChange) +$emailInput.addEventListener('input', handleEmailChange) + +const render: Parameters[0] = (state) => { + const person = { + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + } + + $firstNameInput.value = person.firstName + $lastNameInput.value = person.lastName + $emailInput.value = person.email + + $result.innerHTML = `${person.firstName} ${person.lastName} (${person.email})` +} + +render(personStore.getInitialState(), personStore.getInitialState()) + +personStore.subscribe(render) +``` diff --git a/docs/reference/create-with-equality-fn.md b/docs/reference/create-with-equality-fn.md new file mode 100644 index 0000000000..49e4e10827 --- /dev/null +++ b/docs/reference/create-with-equality-fn.md @@ -0,0 +1,622 @@ +--- +title: createWithEqualityFn +description: +nav: 203 +--- + +# createWithEqualityFn ⚛️ + +`createWithEqualityFn` lets you create a React Hook with API utilities attached, just like `create`. +However, it offers a way to define a custom equality check. This allows for more granular control +over when components re-render, improving performance and responsiveness. + +```js +createWithEqualityFn(stateCreatorFn, equalityFn) +``` + +- [Reference](#reference) + - [Signature](#createwithequalityfn-signature) +- [Usage](#usage) + - [Updating state based on previous state](#updating-state-based-on-previous-state) + - [Updating Primitives in State](#updating-primitives-in-state) + - [Updating Objects in State](#updating-objects-in-state) + - [Updating Arrays in State](#updating-arrays-in-state) + - [Updating state with no store actions](#updating-state-with-no-store-actions) + - [Subscribing to state updates](#subscribing-to-state-updates) +- [Troubleshooting](#troubleshooting) + - [I’ve updated the state, but the screen doesn’t update](#ive-updated-the-state-but-the-screen-doesnt-update) + +## Reference + +### `createWithEqualityFn` Signature + +```ts +createWithEqualityFn()(stateCreatorFn: StateCreator, equalityFn?: (a: T, b: T) => boolean): UseBoundStore> +``` + +#### Parameters + +- `stateCreatorFn`: The state creator function that specifies how the state gets initialized and + updated. It must be pure, should take `setState` function, `getState` function and `storeApi` as + arguments. +- **optional** `equalityFn`: Defaults to `Object.is`. A function that lets you skip re-renders. + +#### Returns + +`createWithEqualityFn` returns a React Hook: + +1. The React Hook that lets you return data that is based on current state, using a `selector` + function, and lets you skip re-renders using a `equality` function. It should take a selector + function, and an equality function as arguments. + +### Updating state based on previous state + +To update a state based on previous state we should use **updater functions**. Read more +about that [here](https://react.dev/learn/queueing-a-series-of-state-updates). + +This example show you how you can support **updater functions** for your **actions**. + +```tsx +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +type AgeStoreState = { age: number } + +type AgeStoreActions = { + setAge: ( + nextAge: + | AgeStoreState['age'] + | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), + ) => void +} + +type AgeStore = AgeStoreState & AgeStoreActions + +const useAgeStore = createWithEqualityFn()( + (set) => ({ + age: 42, + setAge: (nextAge) => { + set((state) => ({ + age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, + })) + }, + }), + shallow, +) + +export default function App() { + const [age, setAge] = useAgeStore((state) => [state.age, state.setAge]) + + function increment() { + setAge((currentAge) => currentAge + 1) + } + + return ( + <> +

Your age: {age}

+ + + + ) +} +``` + +### Updating Primitives in State + +State can hold any kind of JavaScript value. When you want to update built-in primitive values like +number, strings, booleans, etc. we should directly assign new values to ensure updates are applied +correctly, and avoid unexpected behaviors. + +> [!NOTE] +> By default, `set` function performs a shallow merge. If you need to completely replace +> the state with a new one, use the `replace` parameter set to `true` + +```tsx +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +type XStore = number + +const useXStore = createWithEqualityFn()(() => 0, shallow) + +export default function MovingDot() { + const x = useXStore() + const setX = (nextX: number) => { + useXStore.setState(nextX, true) + } + const position = { y: 0, x } + + return ( +
{ + setX(e.clientX) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +### Updating Objects in State + +Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store +them in state. Instead, when you want to update an object, you need to create a new one (or make a +copy of an existing one), and then set the state to use the new object. + +By default, `set` function performs a shallow merge. For most updates where you only need to modify +specific properties, the default shallow merge is preferred as it's more efficient. To completely +replace the state with a new one, use the `replace` parameter set to `true` with caution, as it +discards any existing nested data within the state. + +```tsx +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const usePositionStore = createWithEqualityFn()( + (set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, + }), + shallow, +) + +export default function MovingDot() { + const [position, setPosition] = usePositionStore((state) => [ + { x: state.x, y: state.y }, + state.setPosition, + ]) + + return ( +
{ + setPosition({ + x: e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +### Updating Arrays in State + +Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in +state. Just like with objects, when you want to update an array stored in state, you need to create +a new one (or make a copy of an existing one), and then set state to use the new array. + +By default, `set` function performs a shallow merge. To update array values we should assign new +values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely +replace the state with a new one, use the `replace` parameter set to `true`. + +> [!IMPORTANT ] +> We should prefer immutable operations like: `concat(...)`, `[...array]`, `filter(...)`, +> `slice(...)`, `map(...)`, `[...array].sort(...)`, and `[...array].reverse(...)`, and avoid +> mutable operations like `push(...)`, `unshift(...)`, `pop(...)`, `shift(...)`, `splice(...)`, +> `array[arrayIndex] = ...`, `reverse(...)`, and `sort(...)`. + +```tsx +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +type PositionStore = [number, number] + +const usePositionStore = createWithEqualityFn()( + () => [0, 0], + shallow, +) + +export default function MovingDot() { + const [x, y] = usePositionStore() + const setPosition: typeof usePositionStore.setState = (nextPosition) => { + usePositionStore.setState(nextPosition, true) + } + const position = { x, y } + + return ( +
{ + setPosition([e.clientX, e.clientY]) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +### Updating state with no store actions + +Defining actions at module level, external to the store have a few advantages like: it doesn't +require a hook to call an action, and it facilitates code splitting. + +> [!NOTE] +> The recommended way is to colocate actions and states within the store (let your actions be +> located together with your state). + +```tsx +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +const usePositionStore = createWithEqualityFn<{ + x: number + y: number +}>()(() => ({ x: 0, y: 0 }), shallow) + +const setPosition: typeof usePositionStore.setState = (nextPosition) => { + usePositionStore.setState(nextPosition) +} + +export default function MovingDot() { + const position = usePositionStore() + + return ( +
+
{ + const parent = event.currentTarget.parentElement + const parentWidth = parent.clientWidth + const parentHeight = parent.clientHeight + + setPosition({ + x: Math.ceil(Math.random() * parentWidth), + y: Math.ceil(Math.random() * parentHeight), + }) + }} + /> +
+ ) +} +``` + +### Subscribing to state updates + +By subscribing to state updates, you register a callback that fires whenever the store's state +updates. We can use `subscribe` for external state management. + +```tsx +import { useEffect } from 'react' +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nextPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const usePositionStore = createWithEqualityFn()( + (set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, + }), + shallow, +) + +export default function MovingDot() { + const [position, setPosition] = usePositionStore((state) => [ + { x: state.x, y: state.y }, + state.setPosition, + ]) + + useEffect(() => { + const unsubscribePositionStore = usePositionStore.subscribe(({ x, y }) => { + console.log('new position', { position: { x, y } }) + }) + + return () => { + unsubscribePositionStore() + } + }, []) + + return ( +
+
{ + const parent = event.currentTarget.parentElement + const parentWidth = parent.clientWidth + const parentHeight = parent.clientHeight + + setPosition({ + x: Math.ceil(Math.random() * parentWidth), + y: Math.ceil(Math.random() * parentHeight), + }) + }} + /> +
+ ) +} +``` + +## Troubleshooting + +### I’ve updated the state, but the screen doesn’t update + +In the previous example, the `position` object is always created fresh from the current cursor +position. But often, you will want to include existing data as a part of the new object you’re +creating. For example, you may want to update only one field in a form, but keep the previous +values for all other fields. + +These input fields don’t work because the `onChange` handlers mutate the state: + +```tsx +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +type PersonStoreState = { + firstName: string + lastName: string + email: string +} + +type PersonStoreActions = { + setPerson: (nextPerson: Partial) => void +} + +type PersonStore = PersonStoreState & PersonStoreActions + +const usePersonStore = createWithEqualityFn()( + (set) => ({ + firstName: 'Barbara', + lastName: 'Hepworth', + email: 'bhepworth@sculpture.com', + setPerson: (nextPerson) => { + set(nextPerson) + }, + }), + shallow, +) + +export default function Form() { + const [person] = usePersonStore((state) => [ + { + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + }, + state.setPerson, + ]) + + function handleFirstNameChange(e) { + person.firstName = e.target.value + } + + function handleLastNameChange(e) { + person.lastName = e.target.value + } + + function handleEmailChange(e) { + person.email = e.target.value + } + + return ( + <> + + + +

+ {person.firstName} {person.lastName} ({person.email}) +

+ + ) +} +``` + +For example, this line mutates the state from a past render: + +```tsx +person.firstName = e.target.value +``` + +The reliable way to get the behavior you’re looking for is to create a new object and pass it to +`setPerson`. But here, you want to also copy the existing data into it because only one of the +fields has changed: + +```ts +setPerson({ + firstName: e.target.value, // New first name from the input +}) +``` + +> [!NOTE] +> We don’t need to copy every property separately due to `set` function performs shallow +> merge by default. + +Now the form works! + +Notice how you didn’t declare a separate state variable for each input field. For large forms, +keeping all data grouped in an object is very convenient—as long as you update it correctly! + +```tsx +import { createWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/vanilla/shallow' + +type PersonStoreState = { + firstName: string + lastName: string + email: string +} + +type PersonStoreActions = { + setPerson: (nextPerson: Partial) => void +} + +type PersonStore = PersonStoreState & PersonStoreActions + +const usePersonStore = createWithEqualityFn()( + (set) => ({ + firstName: 'Barbara', + lastName: 'Hepworth', + email: 'bhepworth@sculpture.com', + setPerson: (nextPerson) => { + set(nextPerson) + }, + }), + shallow, +) + +export default function Form() { + const [person, setPerson] = usePersonStore((state) => [ + { + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + }, + state.setPerson, + ]) + + function handleFirstNameChange(e) { + setPerson({ firstName: e.target.value }) + } + + function handleLastNameChange(e) { + setPerson({ lastName: e.target.value }) + } + + function handleEmailChange(e) { + setPerson({ email: e.target.value }) + } + + return ( + <> + + + +

+ {person.firstName} {person.lastName} ({person.email}) +

+ + ) +} +``` diff --git a/docs/reference/create.md b/docs/reference/create.md new file mode 100644 index 0000000000..b9992d01ee --- /dev/null +++ b/docs/reference/create.md @@ -0,0 +1,595 @@ +--- +title: create +description: +nav: 204 +--- + +# create ⚛️ + +`create` lets you create a React Hook with API utilities attached. + +```js +create(stateCreatorFn) +``` + +- [Reference](#reference) + - [Signature](#create-signature) +- [Usage](#usage) + - [Updating state based on previous state](#updating-state-based-on-previous-state) + - [Updating Primitives in State](#updating-primitives-in-state) + - [Updating Objects in State](#updating-objects-in-state) + - [Updating Arrays in State](#updating-arrays-in-state) + - [Updating state with no store actions](#updating-state-with-no-store-actions) + - [Subscribing to state updates](#subscribing-to-state-updates) +- [Troubleshooting](#troubleshooting) + - [I’ve updated the state, but the screen doesn’t update](#ive-updated-the-state-but-the-screen-doesnt-update) + +## Reference + +### `create` Signature + +```ts +create()(stateCreatorFn: StateCreator): UseBoundStore> +``` + +#### Parameters + +- `stateCreatorFn`: The value you want the state to be initially. It can be a value of any type, + but when you pass a function should take `setState` function, `getState` function and `storeApi` + as arguments. + +#### Returns + +`create` returns a React Hook with Store API attached: + +1. The React Hook that lets you return data that is based on current state, using a + [`selector` function](#selector-function). It should take a selector function as its only + argument. + +## Usage + +### Updating state based on previous state + +To update a state based on previous state we should use **updater functions**. Read more +about that [here](https://react.dev/learn/queueing-a-series-of-state-updates). + +This example show you how you can support **updater functions** for your **actions**. + +```tsx +import { create } from 'zustand' + +type AgeStoreState = { age: number } + +type AgeStoreActions = { + setAge: ( + nextAge: + | AgeStoreState['age'] + | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), + ) => void +} + +type AgeStore = AgeStoreState & AgeStoreActions + +const useAgeStore = create()((set) => ({ + age: 42, + setAge: (nextAge) => { + set((state) => ({ + age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, + })) + }, +})) + +export default function App() { + const [age, setAge] = useAgeStore((state) => [state.age, state.setAge]) + + function increment() { + setAge((currentAge) => currentAge + 1) + } + + return ( + <> +

Your age: {age}

+ + + + ) +} +``` + +### Updating Primitives in State + +State can hold any kind of JavaScript value. When you want to update built-in primitive values like +number, strings, booleans, etc. we should directly assign new values to ensure updates are applied +correctly, and avoid unexpected behaviors. + +> [!NOTE] +> By default, `set` function performs a shallow merge. If you need to completely replace the state +> with a new one, use the `replace` parameter set to `true` + +```tsx +import { create } from 'zustand' + +type XStore = number + +const useXStore = create()(() => 0) + +export default function MovingDot() { + const x = useXStore() + const setX = (nextX: number) => { + useXStore.setState(nextX, true) + } + const position = { y: 0, x } + + return ( +
{ + setX(e.clientX) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +### Updating Objects in State + +Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store +them in state. Instead, when you want to update an object, you need to create a new one (or make a +copy of an existing one), and then set the state to use the new object. + +By default, `set` function performs a shallow merge. For most updates where you only need to modify +specific properties, the default shallow merge is preferred as it's more efficient. To completely +replace the state with a new one, use the `replace` parameter set to `true` with caution, as it +discards any existing nested data within the state. + +```tsx +import { create } from 'zustand' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const usePositionStore = create()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) + +export default function MovingDot() { + const [position, setPosition] = usePositionStore((state) => [ + { x: state.x, y: state.y }, + state.setPosition, + ]) + + return ( +
{ + setPosition({ + x: e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +### Updating Arrays in State + +Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in +state. Just like with objects, when you want to update an array stored in state, you need to create +a new one (or make a copy of an existing one), and then set state to use the new array. + +By default, `set` function performs a shallow merge. To update array values we should assign new +values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely +replace the state with a new one, use the `replace` parameter set to `true`. + +> [!IMPORTANT] +> We should prefer immutable operations like: `concat(...)`, `[...array]`, `filter(...)`, +> `slice(...)`, `map(...)`, `[...array].sort(...)`, and `[...array].reverse(...)`, and avoid +> mutable operations like `push(...)`, `unshift(...)`, `pop(...)`, `shift(...)`, `splice(...)`, +> `array[arrayIndex] = ...`, `reverse(...)`, and `sort(...)`. + +```tsx +import { create } from 'zustand' + +type PositionStore = [number, number] + +const usePositionStore = create()(() => [0, 0]) + +export default function MovingDot() { + const [x, y] = usePositionStore() + const setPosition: typeof usePositionStore.setState = (nextPosition) => { + usePositionStore.setState(nextPosition, true) + } + const position = { x, y } + + return ( +
{ + setPosition([e.clientX, e.clientY]) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +### Updating state with no store actions + +Defining actions at module level, external to the store have a few advantages like: it doesn't +require a hook to call an action, and it facilitates code splitting. + +> [!NOTE] +> The recommended way is to colocate actions and states within the store (let your actions be +> located together with your state). + +```tsx +import { create } from 'zustand' + +const usePositionStore = create<{ + x: number + y: number +}>()(() => ({ x: 0, y: 0 })) + +const setPosition: typeof usePositionStore.setState = (nextPosition) => { + usePositionStore.setState(nextPosition) +} + +export default function MovingDot() { + const position = usePositionStore() + + return ( +
+
{ + const parent = event.currentTarget.parentElement + const parentWidth = parent.clientWidth + const parentHeight = parent.clientHeight + + setPosition({ + x: Math.ceil(Math.random() * parentWidth), + y: Math.ceil(Math.random() * parentHeight), + }) + }} + /> +
+ ) +} +``` + +### Subscribing to state updates + +By subscribing to state updates, you register a callback that fires whenever the store's state +updates. We can use `subscribe` for external state management. + +```tsx +import { useEffect } from 'react' +import { create } from 'zustand' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nextPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const usePositionStore = create()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) + +export default function MovingDot() { + const [position, setPosition] = usePositionStore((state) => [ + { x: state.x, y: state.y }, + state.setPosition, + ]) + + useEffect(() => { + const unsubscribePositionStore = usePositionStore.subscribe(({ x, y }) => { + console.log('new position', { position: { x, y } }) + }) + + return () => { + unsubscribePositionStore() + } + }, []) + + return ( +
+
{ + const parent = event.currentTarget.parentElement + const parentWidth = parent.clientWidth + const parentHeight = parent.clientHeight + + setPosition({ + x: Math.ceil(Math.random() * parentWidth), + y: Math.ceil(Math.random() * parentHeight), + }) + }} + /> +
+ ) +} +``` + +## Troubleshooting + +### I’ve updated the state, but the screen doesn’t update + +In the previous example, the `position` object is always created fresh from the current cursor +position. But often, you will want to include existing data as a part of the new object you’re +creating. For example, you may want to update only one field in a form, but keep the previous +values for all other fields. + +These input fields don’t work because the `onChange` handlers mutate the state: + +```tsx +import { create } from 'zustand' + +type PersonStoreState = { + firstName: string + lastName: string + email: string +} + +type PersonStoreActions = { + setPerson: (nextPerson: Partial) => void +} + +type PersonStore = PersonStoreState & PersonStoreActions + +const usePersonStore = create()((set) => ({ + firstName: 'Barbara', + lastName: 'Hepworth', + email: 'bhepworth@sculpture.com', + setPerson: (nextPerson) => { + set(nextPerson) + }, +})) + +export default function Form() { + const [person] = usePersonStore((state) => [ + { + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + }, + state.setPerson, + ]) + + function handleFirstNameChange(e) { + person.firstName = e.target.value + } + + function handleLastNameChange(e) { + person.lastName = e.target.value + } + + function handleEmailChange(e) { + person.email = e.target.value + } + + return ( + <> + + + +

+ {person.firstName} {person.lastName} ({person.email}) +

+ + ) +} +``` + +For example, this line mutates the state from a past render: + +```tsx +person.firstName = e.target.value +``` + +The reliable way to get the behavior you’re looking for is to create a new object and pass it to +`setPerson`. But here, you want to also copy the existing data into it because only one of the +fields has changed: + +```ts +setPerson({ + firstName: e.target.value, // New first name from the input +}) +``` + +> [!NOTE] +> We don’t need to copy every property separately due to `set` function performs shallow merge by +> default. + +Now the form works! + +Notice how you didn’t declare a separate state variable for each input field. For large forms, +keeping all data grouped in an object is very convenient—as long as you update it correctly! + +```tsx +import { create } from 'zustand' + +type PersonStoreState = { + firstName: string + lastName: string + email: string +} + +type PersonStoreActions = { + setPerson: (nextPerson: Partial) => void +} + +type PersonStore = PersonStoreState & PersonStoreActions + +const usePersonStore = create()((set) => ({ + firstName: 'Barbara', + lastName: 'Hepworth', + email: 'bhepworth@sculpture.com', + setPerson: (nextPerson) => { + set(nextPerson) + }, +})) + +export default function Form() { + const [person, setPerson] = usePersonStore((state) => [ + { + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + }, + state.setPerson, + ]) + + function handleFirstNameChange(e) { + setPerson({ firstName: e.target.value }) + } + + function handleLastNameChange(e) { + setPerson({ lastName: e.target.value }) + } + + function handleEmailChange(e) { + setPerson({ email: e.target.value }) + } + + return ( + <> + + + +

+ {person.firstName} {person.lastName} ({person.email}) +

+ + ) +} +``` diff --git a/docs/reference/devtools.md b/docs/reference/devtools.md new file mode 100644 index 0000000000..cadbb9f61f --- /dev/null +++ b/docs/reference/devtools.md @@ -0,0 +1,63 @@ +--- +title: devtools +description: +nav: 205 +--- + +# devtools + +`devtools` middleware lets you use [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) +without Redux. Read more about the benefits of using [Redux DevTools for debugging](https://redux.js.org/style-guide/#use-the-redux-devtools-extension-for-debugging). + +```js +devtools(stateCreatorFn, devtoolsOptions) +``` + +- [Reference](#reference) + - [Signature](#devtools-signature) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) + - [Only one store is displayed](#only-one-store-is-displayed) + - [Action names are labeled as 'anonymous'](#all-action-names-are-labeled-as-anonymous) + +## Reference + +### `devtools` Signature + +```ts +devtools(stateCreatorFn: StateCreator, devtoolsOptions?: DevtoolsOptions): StateCreator +``` + +#### Parameters + +- `stateCreatorFn`: The state creator function that specifies how the state gets initialized and + updated. It must be pure, should take `setState` function, `getState` function and `storeApi` as + arguments. +- **optional** `devtoolsOptions`: An object to define Redux DevTools options. + - **optional** `name`: A custom identifier for the connection in the Redux DevTools. + - **optional** `enabled`: Defaults to `true` when is on development mode, and defaults to `false` + when is on production mode. Enables or disables the Redux DevTools integration + for this store. + - **optional** `anonymousActionType`: Defaults to `anonymous`. A string to use as the action type + for anonymous mutations in the Redux DevTools. + - **optional** `store`: A custom identifier for the store in the Redux DevTools. + +#### Returns + +`devtools` returns a state creator function. + +## Usage + +## Troubleshooting + +### Only one store is displayed + +Lorem ipsum dolor sit amet consectetur adipisicing elit. Illo voluptatum, eos suscipit explicabo +animi ad porro vitae vel ullam saepe magnam in facilis earum, nulla officia sit. Unde, nostrum +delectus! + +### All action names are labeled as 'anonymous' + +Lorem ipsum dolor sit, amet consectetur adipisicing elit. Placeat et illo hic architecto deleniti +soluta, veritatis reiciendis nesciunt laborum laudantium, dolorum asperiores fuga at accusamus aut +facere ex perspiciatis qui! diff --git a/docs/reference/immer.md b/docs/reference/immer.md new file mode 100644 index 0000000000..9f44a4bae1 --- /dev/null +++ b/docs/reference/immer.md @@ -0,0 +1,89 @@ +--- +title: immer +description: +nav: 206 +--- + +# immer + +`immer` middleware lets you perform immutable updates. + +```js +immer(stateCreatorFn) +``` + +- [Reference](#reference) + - [Signature](#immer-signature) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) + +## Reference + +### `immer` Signature + +```ts +immer(stateCreatorFn: StateCreator): StateCreator +``` + +#### Parameters + +- `stateCreatorFn`: The state creator function that specifies how the state gets initialized and + updated. It must be pure, should take `setState` function, `getState` function and `storeApi` as + arguments. + +#### Returns + +`immer` returns a state creator function. + +### `setState` function + +The `setState` function lets you update the state to a different value and trigger re-render. You +can pass the next state directly, a next partial state, a function that calculates it from the +previous state, or replace it completely. + +#### Parameters + +- `nextState`: The value that you want the state to be. It can be a value of any type, but there is + a special behavior for functions. + - If you pass an object as a `nextState`. It will shallow merge `nextState` with the current + state. You can pass only the properties you want to update, this allows for selective state + updates without modifying other properties. + - If you pass a non-object as a `nextState`, make sure you use `replace` as `true` to avoid + unexpected behaviors. + - If you pass a function as a `nextState`. It must be pure, should take current state as its + only argument, and should return the next state. The next state returned by the updater + function face the same restrictions of any next state. +- `replace`: This optional boolean flag controls whether the state is completely replaced or only + shallow updated, through a shallow merge. + +#### Returns + +`setState` function do not have a return value. + +### `getState` function + +The `getState` function lets you access to the current state. It can be stale on asynchronous +operations. + +### `subscribe` function + +The `subscribe` function lets you subscribe to state updates. It should take current state, and +its previous state as arguments. + +#### Parameters + +- `currentState`: The current state. +- `previousState`: The previous state. + +#### Returns + +`subscribe` returns a function that lets you unsubscribe from itself. + +### `storeApi` + +The `storeApi` lets you access to the store API utilities. These store API utilities are: +`setState` function, `getState` function, and `subscribe` function. + +## Usage + +## Troubleshooting diff --git a/docs/reference/persist.md b/docs/reference/persist.md new file mode 100644 index 0000000000..9600d414e2 --- /dev/null +++ b/docs/reference/persist.md @@ -0,0 +1,72 @@ +--- +title: persist Middleware +description: +nav: 207 +--- + +# persist + +`persist` middleware lets you persist your store's state across page reloads or application +restarts. + +```js +persist(stateCreatorFn, persistOptions) +``` + +- [Reference](#reference) + - [Signature](#persist-signature) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) + - [Cannot read property `setOptions` of undefined](#cannot-read-property-setoptions-of-undefined) + - [Storage is not being validated or type checked](#storage-is-not-being-validated-or-type-checked) + - [Cannot read property `setItem`/`getItem`/`removeItem` of null](#cannot-read-property-setitemgetitemremoveitem-of-null) + +## Reference + +### `persist` Signature + +```ts +persist(stateCreatorFn: StateCreator, persistOptions?: PersistOptions): StateCreator +``` + +#### Parameters + +- `stateCreatorFn`: The state creator function that specifies how the state gets initialized and + updated. It must be pure, should take `setState` function, `getState` function and `storeApi` as + arguments. +- `persistOptions`: An object to. + - `name`: A unique name of the item for your store in the storage. + - **optional** `storage`: Defaults to `createJSONStorage(() => localStorage)`. - + - **optional** `partialize`: A function to filter state fields before persisting it. + - **optional** `onRehydrateStorage`: A function or function returning a function that allows + custom logic before and after state rehydration. + - **optional** `version`: A version number for the persisted state. If the stored state version + doesn't match, it won't be used. + - **optional** `migrate`: A function to migrate persisted state if the version mismatch occurs. + - **optional** `merge`: A function for custom logic when merging persisted state with the current + state during rehydration. Defaults to a shallow merge. + - **optional** `skipHydration`: Defaults to `false`. If `true`, the middleware won't + automatically rehydrate the state on initialization. Use `rehydrate` function manually in this + case. This is useful for server-side rendering (SSR) applications. + +#### Returns + +`persist` returns a state creator function. + +## Usage + +## Troubleshooting + +### Cannot read property `setOptions` of undefined + +Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum reprehenderit eaque excepturi, +cumque officia incidunt repellendus, fugit soluta dolore perspiciatis laudantium voluptatem +repudiandae illum ipsum quam, perferendis iusto a aperiam! + +### Storage is not being validated or type checked + +Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum ratione aspernatur dolores ut in +erferendis quis reprehenderit iure numquam minima laborum vero unde, eum laudantium! Minima beatae +natus tempore est. + +### Cannot read property `setItem`/`getItem`/`removeItem` of null diff --git a/docs/reference/redux.md b/docs/reference/redux.md new file mode 100644 index 0000000000..38622c107b --- /dev/null +++ b/docs/reference/redux.md @@ -0,0 +1,90 @@ +--- +title: redux +description: +nav: 208 +--- + +# redux + +`redux` middleware lets you inspect and debug your state from within the browser, using the redux +devtools browser extension. + +```js +redux(reducer, initialState) +``` + +- [Reference](#reference) + - [Signature](#redux-signature) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) + +## Reference + +### `redux` Signature + +```ts +redux(reducer: (state: T, action: A) => T, initialState: T): StateCreator A }, [], []> +``` + +#### Parameters + +- `reducer`: It should be pure and should take the current state of your application and an action + object as arguments, and returns the new state resulting from applying the action. +- `initialState`: It should be the initial state of your application data managed by the store. + +#### Returns + +`redux` returns a state creator function. + +### `setState` function + +The `setState` function lets you update the state to a different value and trigger re-render. You +can pass the next state directly, a next partial state, a function that calculates it from the +previous state, or replace it completely. + +#### Parameters + +- `nextState`: The value that you want the state to be. It can be a value of any type, but there is + a special behavior for functions. + - If you pass an object as a `nextState`. It will shallow merge `nextState` with the current + state. You can pass only the properties you want to update, this allows for selective state + updates without modifying other properties. + - If you pass a non-object as a `nextState`, make sure you use `replace` as `true` to avoid + unexpected behaviors. + - If you pass a function as a `nextState`. It must be pure, should take current state as its + only argument, and should return the next state. The next state returned by the updater + function face the same restrictions of any next state. +- `replace`: This optional boolean flag controls whether the state is completely replaced or only + shallow updated, through a shallow merge. + +#### Returns + +`setState` function do not have a return value. + +### `getState` function + +The `getState` function lets you access to the current state. It can be stale on asynchronous +operations. + +### `subscribe` function + +The `subscribe` function lets you subscribe to state updates. It should take current state, and +its previous state as arguments. + +#### Parameters + +- `currentState`: The current state. +- `previousState`: The previous state. + +#### Returns + +`subscribe` returns a function that lets you unsubscribe from itself. + +### `storeApi` + +The `storeApi` lets you access to the store API utilities. These store API utilities are: +`setState` function, `getState` function, and `subscribe` function. + +## Usage + +## Troubleshooting diff --git a/docs/reference/shallow.md b/docs/reference/shallow.md new file mode 100644 index 0000000000..933d53fb41 --- /dev/null +++ b/docs/reference/shallow.md @@ -0,0 +1,223 @@ +--- +title: shallow +description: +nav: 209 +--- + +# shallow + +`shallow` lets you run fast checks on simple data structures. It effectively identifies changes in +**top-level** properties when you're working with data structures that don't have nested objects or +arrays within them. + +> [!NOTE] +> Shallow lets you perform quick comparisons, but keep its limitations in mind. + +```js +shallow(a, b) +``` + +- [Reference](#reference) + - [Signature](#shallow-signature) +- [Usage](#usage) + - [Comparing Primitives](#comparing-primitives) + - [Comparing Objects](#comparing-objects) + - [Comparing Sets](#comparing-sets) + - [Comparing Maps](#comparing-maps) +- [Troubleshooting](#troubleshooting) + - [Comparing objects returns `false` even if they are identical.](#comparing-objects-returns-false-even-if-they-are-identical) + +## Reference + +### `shallow` Signature + +```ts +shallow(a: T, b: T): boolean +``` + +#### Parameters + +- `a`: The first value. +- `b`: The second value. + +#### Returns + +`shallow` returns `true` when `a` and `b` are equal based on a shallow comparison of their +**top-level** properties. Otherwise, it should return `false`. + +## Usage + +### Comparing Primitives + +When comparing primitive values like `string`s, `number`s, `boolean`s, and `BigInt`s, both +`Object.is` and `shallow` function return `true` if the values are the same. This is because +primitive values are compared by their actual value rather than by reference. + +```ts +const stringLeft = 'John Doe' +const stringRight = 'John Doe' + +Object.is(stringLeft, stringRight) // -> true +shallow(stringLeft, stringRight) // -> true + +const numberLeft = 10 +const numberRight = 10 + +Object.is(numberLeft, numberRight) // -> true +shallow(numberLeft, numberRight) // -> true + +const booleanLeft = true +const booleanRight = true + +Object.is(booleanLeft, booleanRight) // -> true +shallow(booleanLeft, booleanRight) // -> true + +const bigIntLeft = 1n +const bigIntRight = 1n + +Object.is(bigInLeft, bigInRight) // -> true +shallow(bigInLeft, bigInRight) // -> true +``` + +### Comparing Objects + +When comparing objects, it's important to understand how `Object.is` and `shallow` function +operate, as they handle comparisons differently. + +The `shallow` function returns `true` because shallow performs a shallow comparison of the objects. +It checks if the top-level properties and their values are the same. In this case, the top-level +properties (`firstName`, `lastName`, and `age`) and their values are identical between `objectLeft` +and `objectRight`, so shallow considers them equal. + +```ts +const objectLeft = { + firstName: 'John', + lastName: 'Doe', + age: 30, +} +const objectRight = { + firstName: 'John', + lastName: 'Doe', + age: 30, +} + +Object.is(objectLeft, objectRight) // -> false +shallow(objectLeft, objectRight) // -> true +``` + +### Comparing Sets + +When comparing sets, it's important to understand how `Object.is` and `shallow` function operate, +as they handle comparisons differently. + +The `shallow` function returns `true` because shallow performs a shallow comparison of the sets. It +checks if the top-level properties (in this case, the sets themselves) are the same. Since `setLeft` +and `setRight` are both instances of the Set object and contain the same elements, shallow considers +them equal. + +```ts +const setLeft = new Set([1, 2, 3]) +const setRight = new Set([1, 2, 3]) + +Object.is(setLeft, setRight) // -> false +shallow(setLeft, setRight) // -> true +``` + +### Comparing Maps + +When comparing maps, it's important to understand how `Object.is` and `shallow` function operate, as +they handle comparisons differently. + +The `shallow` returns `true` because shallow performs a shallow comparison of the maps. It checks if +the top-level properties (in this case, the maps themselves) are the same. Since `mapLeft` and +`mapRight` are both instances of the Map object and contain the same key-value pairs, shallow +considers them equal. + +```ts +const mapLeft = new Map([ + [1, 'one'], + [2, 'two'], + [3, 'three'], +]) +const mapRight = new Map([ + [1, 'one'], + [2, 'two'], + [3, 'three'], +]) + +Object.is(mapLeft, mapRight) // -> false +shallow(mapLeft, mapRight) // -> true +``` + +## Troubleshooting + +### Comparing objects returns `false` even if they are identical. + +The `shallow` function performs a shallow comparison. A shallow comparison checks if the top-level +properties of two objects are equal. It does not check nested objects or deeply nested properties. +In other words, it only compares the references of the properties. + +In the following example, the shallow function returns `false` because it compares only the +top-level properties and their references. The address property in both objects is a nested object, +and even though their contents are identical, their references are different. Consequently, shallow +sees them as different, resulting in `false`. + +```ts +const objectLeft = { + firstName: 'John', + lastName: 'Doe', + age: 30, + address: { + street: 'Kulas Light', + suite: 'Apt. 556', + city: 'Gwenborough', + zipcode: '92998-3874', + geo: { + lat: '-37.3159', + lng: '81.1496', + }, + }, +} +const objectRight = { + firstName: 'John', + lastName: 'Doe', + age: 30, + address: { + street: 'Kulas Light', + suite: 'Apt. 556', + city: 'Gwenborough', + zipcode: '92998-3874', + geo: { + lat: '-37.3159', + lng: '81.1496', + }, + }, +} + +Object.is(objectLeft, objectRight) // -> false +shallow(objectLeft, objectRight) // -> false +``` + +If we remove the `address` property, the shallow comparison would work as expected because all +top-level properties would be primitive values or references to the same values: + +```ts +const objectLeft = { + firstName: 'John', + lastName: 'Doe', + age: 30, +} +const objectRight = { + firstName: 'John', + lastName: 'Doe', + age: 30, +} + +Object.is(objectLeft, objectRight) // -> false +shallow(objectLeft, objectRight) // -> true +``` + +In this modified example, `objectLeft` and `objectRight` have the same top-level properties and +primitive values. Since `shallow` function only compares the top-level properties, it will return +`true` because the primitive values (`firstName`, `lastName`, and `age`) are identical in both +objects. diff --git a/docs/reference/subscribe-with-selector.md b/docs/reference/subscribe-with-selector.md new file mode 100644 index 0000000000..24de854f03 --- /dev/null +++ b/docs/reference/subscribe-with-selector.md @@ -0,0 +1,40 @@ +--- +title: subscribeWithSelector +description: +nav: 210 +--- + +# subscribeWithSelector + +`subscribeWithSelector` middleware lets you subscribe to specific data based on current state. + +```js +subscribeWithSelector(stateCreatorFn) +``` + +- [Reference](#reference) + - [Signature](#subscribewithselector-signature) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) + +## Reference + +### `subscribeWithSelector` Signature + +```ts +subscribeWithSelector(stateCreatorFn: StateCreator): StateCreator +``` + +#### Parameters + +- `stateCreatorFn`: The state creator function that specifies how the state gets initialized and + updated. It must be pure, should take `setState` function, `getState` function and `storeApi` as + arguments. + +#### Returns + +`subscribeWithSelector` returns a state creator function. + +## Usage + +## Troubleshooting diff --git a/docs/reference/use-shallow.md b/docs/reference/use-shallow.md new file mode 100644 index 0000000000..846f70e14f --- /dev/null +++ b/docs/reference/use-shallow.md @@ -0,0 +1,262 @@ +--- +title: useShallow +description: +nav: 211 +--- + +# useShallow ⚛️ + +`useShallow` is a React Hook that lets you optimize re-renders. + +```js +useShallow(selector) +``` + +- [Reference](#reference) + - [Signature](#useshallow-signature) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) + +## Reference + +### `useShallow` Signature + +```ts +useShallow(selectorFn: (state: T) => U): (state: T) => U +``` + +#### Parameters + +- `selectorFn`: A function that lets you return data that is based on current state. + +#### Returns + +`useShallow` returns a memoized version of a selector function using a shallow comparison for +memoization. + +## Usage + +### Writing a memoized selector + +First, we need to setup a store to hold the state for the bear family. In this store, we define +three properties: `papaBear`, `mamaBear`, and `babyBear`, each representing a different member of +the bear family and their respective oatmeal pot sizes. + +```tsx +import { create } from 'zustand' + +type BearFamilyMealsStore = { + [key: string]: string +} + +const useBearFamilyMealsStore = create()(() => ({ + papaBear: 'large porridge-pot', + mamaBear: 'middle-size porridge pot', + babyBear: 'A little, small, wee pot', +})) +``` + +Next, we'll create a `BearNames` component that retrieves the keys of our state (the bear family +members) and displays them. + +```tsx +function BearNames() { + const names = useBearFamilyMealsStore((state) => Object.keys(state)) + + return
{names.join(', ')}
+} +``` + +Next, we will create a `UpdateBabyBearMeal` component that periodically updates babe bear's meal +choice. + +```tsx +const meals = [ + 'A tiny, little, wee bowl', + 'A small, petite, tiny pot', + 'A wee, itty-bitty, small bowl', + 'A little, petite, tiny dish', + 'A tiny, small, wee vessel', + 'A small, little, wee cauldron', + 'A little, tiny, small cup', + 'A wee, small, little jar', + 'A tiny, wee, small pan', + 'A small, wee, little crock', +] + +function UpdateBabyBearMeal() { + useEffect(() => { + const timer = setInterval(() => { + useBearFamilyMealsStore.setState({ + tinyBear: meals[Math.floor(Math.random() * (meals.length - 1))], + }) + }, 1000) + + return () => { + clearInterval(timer) + } + }, []) + + return null +} +``` + +Finally, we combine both components in the `App` component to see them in action. + +```tsx +export default function App() { + return ( + <> + + + + ) +} +``` + +Here is what the code should look like: + +```tsx +import { useEffect } from 'react' +import { create } from 'zustand' + +type BearFamilyMealsStore = { + [key: string]: string +} + +const useBearFamilyMealsStore = create()(() => ({ + papaBear: 'large porridge-pot', + mamaBear: 'middle-size porridge pot', + babyBear: 'A little, small, wee pot', +})) + +const meals = [ + 'A tiny, little, wee bowl', + 'A small, petite, tiny pot', + 'A wee, itty-bitty, small bowl', + 'A little, petite, tiny dish', + 'A tiny, small, wee vessel', + 'A small, little, wee cauldron', + 'A little, tiny, small cup', + 'A wee, small, little jar', + 'A tiny, wee, small pan', + 'A small, wee, little crock', +] + +function UpdateBabyBearMeal() { + useEffect(() => { + const timer = setInterval(() => { + useBearFamilyMealsStore.setState({ + tinyBear: meals[Math.floor(Math.random() * (meals.length - 1))], + }) + }, 1000) + + return () => { + clearInterval(timer) + } + }, []) + + return null +} + +function BearNames() { + const names = useBearFamilyMealsStore((state) => Object.keys(state)) + + return
{names.join(', ')}
+} + +export default function App() { + return ( + <> + + + + ) +} +``` + +Everything might look fine, but there’s a small problem: the `BearNames` component keeps +re-rendering even if the names haven’t changed. This happens because the component re-renders +whenever any part of the state changes, even if the specific part we care about (the list of names) hasn’t changed. + +To fix this, we use `useShallow` to make sure the component only re-renders when the actual keys of +the state change: + +```tsx +function BearNames() { + const names = useBearFamilyStore(useShallow((state) => Object.keys(state))) + + return
{names.join(', ')}
+} +``` + +Here is what the code should look like: + +```tsx +import { useEffect } from 'react' +import { create } from 'zustand' +import { useShallow } from 'zustand/react/shallow' + +type BearFamilyMealsStore = { + [key: string]: string +} + +const useBearFamilyMealsStore = create()(() => ({ + papaBear: 'large porridge-pot', + mamaBear: 'middle-size porridge pot', + babyBear: 'A little, small, wee pot', +})) + +const meals = [ + 'A tiny, little, wee bowl', + 'A small, petite, tiny pot', + 'A wee, itty-bitty, small bowl', + 'A little, petite, tiny dish', + 'A tiny, small, wee vessel', + 'A small, little, wee cauldron', + 'A little, tiny, small cup', + 'A wee, small, little jar', + 'A tiny, wee, small pan', + 'A small, wee, little crock', +] + +function UpdateBabyBearMeal() { + useEffect(() => { + const timer = setInterval(() => { + useBearFamilyMealsStore.setState({ + tinyBear: meals[Math.floor(Math.random() * (meals.length - 1))], + }) + }, 1000) + + return () => { + clearInterval(timer) + } + }, []) + + return null +} + +function BearNames() { + const names = useBearFamilyMealsStore( + useShallow((state) => Object.keys(state)), + ) + + return
{names.join(', ')}
+} + +export default function App() { + return ( + <> + + + + ) +} +``` + +By using `useShallow`, we optimized the rendering process, ensuring that the component only +re-renders when necessary, which improves overall performance. + +## Troubleshooting + +TBD diff --git a/docs/reference/use-store-with-equality-fn.md b/docs/reference/use-store-with-equality-fn.md new file mode 100644 index 0000000000..6c8be95dea --- /dev/null +++ b/docs/reference/use-store-with-equality-fn.md @@ -0,0 +1,960 @@ +--- +title: useStoreWithEqualityFn +description: +nav: 212 +--- + +# useStoreWithEqualityFn ⚛️ + +`useStoreWithEqualityFn` is a React Hook that lets you use a vanilla store in React, just like +`useStore`. However, it offers a way to define a custom equality check. This allows for more +granular control over when components re-render, improving performance and responsiveness. + +```js +useStoreWithEqualityFn(storeApi, selectorFn, equalityFn) +``` + +- [Reference](#reference) + - [Signature](#usestorewithequalityfn-signature) +- [Usage](#usage) + - [Use a vanilla store in React](#use-a-vanilla-store-in-react) + - [Using dynamic vanilla stores in React](#using-dynamic-global-vanilla-stores-in-react) + - [Using scoped (non-global) vanilla store in React](#using-scoped-non-global-vanilla-store-in-react) + - [Using dynamic scoped (non-global) vanilla stores in React](#using-dynamic-scoped-non-global-vanilla-stores-in-react) +- [Troubleshooting](#troubleshooting) + - TBD + +## Reference + +### `useStoreWithEqualityFn` Signature + +```ts +useStoreWithEqualityFn(storeApi: StoreApi, selectorFn: (state: T) => U, equalityFn?: (a: T, b: T) => boolean): U +``` + +#### Parameters + +- `storeApi`: The instance that lets you access to store API utilities. +- `selectorFn`: A function that lets you return data that is based on current state. +- `equalityFn`: A function that lets you skip re-renders. + +#### Returns + +`useStoreWithEqualityFn` returns Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint +ipsam iure nesciunt consectetur magnam nulla nostrum ducimus repellendus. Maiores ducimus, expedita +architecto in placeat enim debitis non repudiandae veritatis neque. + +## Usage + +### Using a global vanilla store in React + +First, let's set up a store that will hold the position of the dot on the screen. We'll define the +store to manage `x` and `y` coordinates and provide an action to update these coordinates. + +```tsx +import { createStore, useStore } from 'zustand' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const positionStore = createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) +``` + +Next, we'll create a `MovingDot` component that renders a div representing the dot. This component +will use the store to track and update the dot's position. + +```tsx +function MovingDot() { + const [position, setPosition] = useStoreWithEqualityFn( + positionStore, + (state) => [{ x: state.x, y: state.y }, state.setPosition], + shallow, + ) + + return ( +
{ + setPosition({ + x: e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +Finally, we’ll render the `MovingDot` component in our `App` component. + +```tsx +export default function App() { + return +} +``` + +Here is what the code should look like: + +```tsx +import { createStore } from 'zustand' +import { useStoreWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/shallow' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const positionStore = createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) + +function MovingDot() { + const [position, setPosition] = useStoreWithEqualityFn( + positionStore, + (state) => [{ x: state.x, y: state.y }, state.setPosition], + shallow, + ) + + return ( +
{ + setPosition({ + x: e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} + +export default function App() { + return +} +``` + +### Using dynamic global vanilla stores in React + +First, we'll create a factory function that generates a store for managing the counter state. +Each tab will have its own instance of this store. + +```ts +import { createStore } from 'zustand' + +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} +``` + +Next, we'll create a factory function that manages the creation and retrieval of counter stores. +This allows each tab to have its own independent counter. + +```ts +const defaultCounterStores = new Map< + string, + ReturnType +>() + +const createCounterStoreFactory = ( + counterStores: typeof defaultCounterStores, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} + +const getOrCreateCounterStoreByKey = + createCounterStoreFactory(defaultCounterStores) +``` + +Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s +counter. + +```tsx +const [currentTabIndex, setCurrentTabIndex] = useState(0) +const counterState = useStoreWithEqualityFn( + getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), + (state) => state, + shallow, +) + +return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+) +``` + +Finally, we'll create the `App` component, which renders the tabs and their respective counters. +The counter state is managed independently for each tab. + +```tsx +export default function App() { + return +} +``` + +Here is what the code should look like: + +```tsx +import { useState } from 'react' +import { createStore } from 'zustand' +import { useStoreWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/shallow' + +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} + +const defaultCounterStores = new Map< + string, + ReturnType +>() + +const createCounterStoreFactory = ( + counterStores: typeof defaultCounterStores, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} + +const getOrCreateCounterStoreByKey = + createCounterStoreFactory(defaultCounterStores) + +export default function App() { + const [currentTabIndex, setCurrentTabIndex] = useState(0) + const counterState = useStoreWithEqualityFn( + getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), + (state) => state, + shallow, + ) + + return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+ ) +} +``` + +### Using scoped (non-global) vanilla store in React + +First, let's set up a store that will hold the position of the dot on the screen. We'll define the +store to manage `x` and `y` coordinates and provide an action to update these coordinates. + +```tsx +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nextPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const createPositionStore = () => { + return createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, + })) +} +``` + +Next, we'll create a context and a provider component to pass down the store through the React +component tree. This allows each `MovingDot` component to have its own independent state. + +```tsx +const PositionStoreContext = createContext | null>(null) + +function PositionStoreProvider({ children }: { children: ReactNode }) { + const [positionStore] = useState(createPositionStore) + + return ( + + {children} + + ) +} +``` + +To simplify accessing the store, we’ll create a React custom hook, `usePositionStore`. This hook +will read the store from the context and allow us to select specific parts of the state. + +```ts +function usePositionStore(selector: (state: PositionStore) => U) { + const store = useContext(PositionStoreContext) + + if (store === null) { + throw new Error( + 'usePositionStore must be used within PositionStoreProvider', + ) + } + + return useStoreWithEqualityFn(store, selector, shallow) +} +``` + +Now, let's create the `MovingDot` component, which will render a dot that follows the mouse cursor +within its container. + +```tsx +function MovingDot({ color }: { color: string }) { + const [position, setPosition] = usePositionStore( + (state) => [{ x: state.x, y: state.y }, state.setPosition] as const, + ) + + return ( +
{ + setPosition({ + x: + e.clientX > e.currentTarget.clientWidth + ? e.clientX - e.currentTarget.clientWidth + : e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '50vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` +components, each with its own independent state. + +```tsx +export default function App() { + return ( +
+ + + + + + +
+ ) +} +``` + +Here is what the code should look like: + +```tsx +import { type ReactNode, useState, createContext, useContext } from 'react' +import { createStore } from 'zustand' +import { useStoreWithEqualityFn } from 'zustand/traditional' +import { shallow } from 'zustand/shallow' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const createPositionStore = () => { + return createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, + })) +} + +const PositionStoreContext = createContext | null>(null) + +function PositionStoreProvider({ children }: { children: ReactNode }) { + const [positionStore] = useState(createPositionStore) + + return ( + + {children} + + ) +} + +function usePositionStore(selector: (state: PositionStore) => U) { + const store = useContext(PositionStoreContext) + + if (store === null) { + throw new Error( + 'usePositionStore must be used within PositionStoreProvider', + ) + } + + return useStoreWithEqualityFn(store, selector, shallow) +} + +function MovingDot({ color }: { color: string }) { + const [position, setPosition] = usePositionStore( + (state) => [{ x: state.x, y: state.y }, state.setPosition] as const, + ) + + return ( +
{ + setPosition({ + x: + e.clientX > e.currentTarget.clientWidth + ? e.clientX - e.currentTarget.clientWidth + : e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '50vw', + height: '100vh', + }} + > +
+
+ ) +} + +export default function App() { + return ( +
+ + + + + + +
+ ) +} +``` + +### Using dynamic scoped (non-global) vanilla stores in React + +First, we'll create a factory function that generates a store for managing the counter state. +Each tab will have its own instance of this store. + +```ts +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} +``` + +Next, we'll create a factory function that manages the creation and retrieval of counter stores. +This allows each tab to have its own independent counter. + +```ts +const createCounterStoreFactory = ( + counterStores: Map>, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} +``` + +Next, we need a way to manage and access these stores throughout our app. We’ll use React’s context +for this. + +```tsx +const CounterStoresContext = createContext(null) + +const CounterStoresProvider = ({ children }) => { + const [stores] = useState( + () => new Map>(), + ) + + return ( + {children} + ) +} +``` + +Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a +given tab. + +```tsx +const useCounterStore = ( + currentTabIndex: number, + selector: (state: CounterStore) => U, +) => { + const stores = useContext(CounterStoresContext) + + if (stores === undefined) { + throw new Error('useCounterStore must be used within CounterStoresProvider') + } + + const getOrCreateCounterStoreByKey = useCallback( + () => createCounterStoreFactory(stores), + [stores], + ) + + return useStoreWithEqualityFn( + getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), + selector, + shallow, + ) +} +``` + +Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s +counter. + +```tsx +function Tabs() { + const [currentTabIndex, setCurrentTabIndex] = useState(0) + const counterState = useCounterStore( + `tab-${currentTabIndex}`, + (state) => state, + ) + + return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+ ) +} +``` + +Finally, we'll create the `App` component, which renders the tabs and their respective counters. +The counter state is managed independently for each tab. + +```tsx +export default function App() { + return ( + + + + ) +} +``` + +Here is what the code should look like: + +```tsx +import { + type ReactNode, + useState, + useCallback, + useContext, + createContext, +} from 'react' +import { createStore, useStore } from 'zustand' + +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} + +const createCounterStoreFactory = ( + counterStores: Map>, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} + +const CounterStoresContext = createContext +> | null>(null) + +const CounterStoresProvider = ({ children }: { children: ReactNode }) => { + const [stores] = useState( + () => new Map>(), + ) + + return ( + + {children} + + ) +} + +const useCounterStore = ( + key: string, + selector: (state: CounterStore) => U, +) => { + const stores = useContext(CounterStoresContext) + + if (stores === undefined) { + throw new Error('useCounterStore must be used within CounterStoresProvider') + } + + const getOrCreateCounterStoreByKey = useCallback( + (key: string) => createCounterStoreFactory(stores!)(key), + [stores], + ) + + return useStore(getOrCreateCounterStoreByKey(key), selector) +} + +function Tabs() { + const [currentTabIndex, setCurrentTabIndex] = useState(0) + const counterState = useCounterStore( + `tab-${currentTabIndex}`, + (state) => state, + ) + + return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+ ) +} + +export default function App() { + return ( + + + + ) +} +``` + +## Troubleshooting + +TBD diff --git a/docs/reference/use-store.md b/docs/reference/use-store.md new file mode 100644 index 0000000000..6207a54ff3 --- /dev/null +++ b/docs/reference/use-store.md @@ -0,0 +1,938 @@ +--- +title: useStore +description: +nav: 213 +--- + +# useStore ⚛️ + +`useStore` is a React Hook that lets you use a vanilla store in React. + +```js +useStore(storeApi, selectorFn) +``` + +- [Reference](#reference) + - [Signature](#usestore-signature) +- [Usage](#usage) + - [Use a vanilla store in React](#use-a-vanilla-store-in-react) + - [Using dynamic vanilla stores in React](#using-dynamic-global-vanilla-stores-in-react) + - [Using scoped (non-global) vanilla store in React](#using-scoped-non-global-vanilla-store-in-react) + - [Using dynamic scoped (non-global) vanilla stores in React](#using-dynamic-scoped-non-global-vanilla-stores-in-react) +- [Troubleshooting](#troubleshooting) + - TBD + +## Reference + +### `useStore` Signature + +```ts +useStore, U = T>(storeApi: StoreApi, selectorFn?: (state: T) => U) => UseBoundStore> +``` + +#### Parameters + +- `storeApi`: The instance that lets you access to store API utilities. +- `selectorFn`: A function that lets you return data that is based on current state. + +#### Returns + +`useStore` returns current state or returns any data based on current state depending on the +selector function. + +## Usage + +### Using a global vanilla store in React + +First, let's set up a store that will hold the position of the dot on the screen. We'll define the +store to manage `x` and `y` coordinates and provide an action to update these coordinates. + +```tsx +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const positionStore = createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) +``` + +Next, we'll create a `MovingDot` component that renders a div representing the dot. This component +will use the store to track and update the dot's position. + +```tsx +function MovingDot() { + const [position, setPosition] = useStore(positionStore, (state) => [ + { x: state.x, y: state.y }, + state.setPosition, + ]) + + return ( +
{ + setPosition({ + x: e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +Finally, we’ll render the `MovingDot` component in our `App` component. + +```tsx +export default function App() { + return +} +``` + +Here is what the code should look like: + +```tsx +import { createStore, useStore } from 'zustand' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const positionStore = createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, +})) + +function MovingDot() { + const [position, setPosition] = useStore(positionStore, (state) => [ + { x: state.x, y: state.y }, + state.setPosition, + ]) + + return ( +
{ + setPosition({ + x: e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '100vw', + height: '100vh', + }} + > +
+
+ ) +} + +export default function App() { + return +} +``` + +### Using dynamic global vanilla stores in React + +First, we'll create a factory function that generates a store for managing the counter state. +Each tab will have its own instance of this store. + +```ts +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} +``` + +Next, we'll create a factory function that manages the creation and retrieval of counter stores. +This allows each tab to have its own independent counter. + +```ts +const defaultCounterStores = new Map< + string, + ReturnType +>() + +const createCounterStoreFactory = ( + counterStores: typeof defaultCounterStores, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} + +const getOrCreateCounterStoreByKey = + createCounterStoreFactory(defaultCounterStores) +``` + +Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s +counter. + +```tsx +const [currentTabIndex, setCurrentTabIndex] = useState(0) +const counterState = useStore( + getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), +) + +return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+) +``` + +Finally, we'll create the `App` component, which renders the tabs and their respective counters. +The counter state is managed independently for each tab. + +```tsx +export default function App() { + return +} +``` + +Here is what the code should look like: + +```tsx +import { useState } from 'react' +import { createStore, useStore } from 'zustand' + +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} + +const defaultCounterStores = new Map< + string, + ReturnType +>() + +const createCounterStoreFactory = ( + counterStores: typeof defaultCounterStores, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} + +const getOrCreateCounterStoreByKey = + createCounterStoreFactory(defaultCounterStores) + +export default function App() { + const [currentTabIndex, setCurrentTabIndex] = useState(0) + const counterState = useStore( + getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), + ) + + return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+ ) +} +``` + +### Using scoped (non-global) vanilla store in React + +First, let's set up a store that will hold the position of the dot on the screen. We'll define the +store to manage `x` and `y` coordinates and provide an action to update these coordinates. + +```tsx +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nextPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const createPositionStore = () => { + return createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, + })) +} +``` + +Next, we'll create a context and a provider component to pass down the store through the React +component tree. This allows each `MovingDot` component to have its own independent state. + +```tsx +const PositionStoreContext = createContext | null>(null) + +function PositionStoreProvider({ children }: { children: ReactNode }) { + const [positionStore] = useState(createPositionStore) + + return ( + + {children} + + ) +} +``` + +To simplify accessing the store, we’ll create a React custom hook, `usePositionStore`. This hook +will read the store from the context and allow us to select specific parts of the state. + +```ts +function usePositionStore(selector: (state: PositionStore) => U) { + const store = useContext(PositionStoreContext) + + if (store === null) { + throw new Error( + 'usePositionStore must be used within PositionStoreProvider', + ) + } + + return useStore(store, selector) +} +``` + +Now, let's create the `MovingDot` component, which will render a dot that follows the mouse cursor +within its container. + +```tsx +function MovingDot({ color }: { color: string }) { + const [position, setPosition] = usePositionStore( + (state) => [{ x: state.x, y: state.y }, state.setPosition] as const, + ) + + return ( +
{ + setPosition({ + x: + e.clientX > e.currentTarget.clientWidth + ? e.clientX - e.currentTarget.clientWidth + : e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '50vw', + height: '100vh', + }} + > +
+
+ ) +} +``` + +Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` +components, each with its own independent state. + +```tsx +export default function App() { + return ( +
+ + + + + + +
+ ) +} +``` + +Here is what the code should look like: + +```tsx +import { type ReactNode, useState, createContext, useContext } from 'react' +import { createStore, useStore } from 'zustand' + +type PositionStoreState = { x: number; y: number } + +type PositionStoreActions = { + setPosition: (nexPosition: Partial) => void +} + +type PositionStore = PositionStoreState & PositionStoreActions + +const createPositionStore = () => { + return createStore()((set) => ({ + x: 0, + y: 0, + setPosition: (nextPosition) => { + set(nextPosition) + }, + })) +} + +const PositionStoreContext = createContext | null>(null) + +function PositionStoreProvider({ children }: { children: ReactNode }) { + const [positionStore] = useState(createPositionStore) + + return ( + + {children} + + ) +} + +function usePositionStore(selector: (state: PositionStore) => U) { + const store = useContext(PositionStoreContext) + + if (store === null) { + throw new Error( + 'usePositionStore must be used within PositionStoreProvider', + ) + } + + return useStore(store, selector) +} + +function MovingDot({ color }: { color: string }) { + const [position, setPosition] = usePositionStore( + (state) => [{ x: state.x, y: state.y }, state.setPosition] as const, + ) + + return ( +
{ + setPosition({ + x: + e.clientX > e.currentTarget.clientWidth + ? e.clientX - e.currentTarget.clientWidth + : e.clientX, + y: e.clientY, + }) + }} + style={{ + position: 'relative', + width: '50vw', + height: '100vh', + }} + > +
+
+ ) +} + +export default function App() { + return ( +
+ + + + + + +
+ ) +} +``` + +### Using dynamic scoped (non-global) vanilla stores in React + +First, we'll create a factory function that generates a store for managing the counter state. +Each tab will have its own instance of this store. + +```ts +import { createStore } from 'zustand' + +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} +``` + +Next, we'll create a factory function that manages the creation and retrieval of counter stores. +This allows each tab to have its own independent counter. + +```ts +const createCounterStoreFactory = ( + counterStores: Map>, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} +``` + +Next, we need a way to manage and access these stores throughout our app. We’ll use React’s context +for this. + +```tsx +const CounterStoresContext = createContext(null) + +const CounterStoresProvider = ({ children }) => { + const [stores] = useState( + () => new Map>(), + ) + + return ( + {children} + ) +} +``` + +Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a +given tab. + +```tsx +const useCounterStore = ( + currentTabIndex: number, + selector: (state: CounterStore) => U, +) => { + const stores = useContext(CounterStoresContext) + + if (stores === undefined) { + throw new Error('useCounterStore must be used within CounterStoresProvider') + } + + const getOrCreateCounterStoreByKey = useCallback( + () => createCounterStoreFactory(stores), + [stores], + ) + + return useStore(getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`)) +} +``` + +Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s +counter. + +```tsx +function Tabs() { + const [currentTabIndex, setCurrentTabIndex] = useState(0) + const counterState = useCounterStore( + `tab-${currentTabIndex}`, + (state) => state, + ) + + return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+ ) +} +``` + +Finally, we'll create the `App` component, which renders the tabs and their respective counters. +The counter state is managed independently for each tab. + +```tsx +export default function App() { + return ( + + + + ) +} +``` + +Here is what the code should look like: + +```tsx +import { + type ReactNode, + useState, + useCallback, + useContext, + createContext, +} from 'react' +import { createStore, useStore } from 'zustand' + +type CounterState = { + count: number +} + +type CounterActions = { increment: () => void } + +type CounterStore = CounterState & CounterActions + +const createCounterStore = () => { + return createStore()((set) => ({ + count: 0, + increment: () => { + set((state) => ({ count: state.count + 1 })) + }, + })) +} + +const createCounterStoreFactory = ( + counterStores: Map>, +) => { + return (counterStoreKey: string) => { + if (!counterStores.has(counterStoreKey)) { + counterStores.set(counterStoreKey, createCounterStore()) + } + return counterStores.get(counterStoreKey)! + } +} + +const CounterStoresContext = createContext +> | null>(null) + +const CounterStoresProvider = ({ children }: { children: ReactNode }) => { + const [stores] = useState( + () => new Map>(), + ) + + return ( + + {children} + + ) +} + +const useCounterStore = ( + key: string, + selector: (state: CounterStore) => U, +) => { + const stores = useContext(CounterStoresContext) + + if (stores === undefined) { + throw new Error('useCounterStore must be used within CounterStoresProvider') + } + + const getOrCreateCounterStoreByKey = useCallback( + (key: string) => createCounterStoreFactory(stores!)(key), + [stores], + ) + + return useStore(getOrCreateCounterStoreByKey(key), selector) +} + +function Tabs() { + const [currentTabIndex, setCurrentTabIndex] = useState(0) + const counterState = useCounterStore( + `tab-${currentTabIndex}`, + (state) => state, + ) + + return ( +
+
+ + + +
+
+ Content of Tab {currentTabIndex + 1} +

+ +
+
+ ) +} + +export default function App() { + return ( + + + + ) +} +``` + +## Troubleshooting + +TBD diff --git a/docs/getting-started/comparison.md b/old_docs/getting-started/comparison.md similarity index 100% rename from docs/getting-started/comparison.md rename to old_docs/getting-started/comparison.md diff --git a/docs/getting-started/introduction.md b/old_docs/getting-started/introduction.md similarity index 100% rename from docs/getting-started/introduction.md rename to old_docs/getting-started/introduction.md diff --git a/docs/guides/auto-generating-selectors.md b/old_docs/guides/auto-generating-selectors.md similarity index 100% rename from docs/guides/auto-generating-selectors.md rename to old_docs/guides/auto-generating-selectors.md diff --git a/docs/guides/connect-to-state-with-url-hash.md b/old_docs/guides/connect-to-state-with-url-hash.md similarity index 100% rename from docs/guides/connect-to-state-with-url-hash.md rename to old_docs/guides/connect-to-state-with-url-hash.md diff --git a/docs/guides/event-handler-in-pre-react-18.md b/old_docs/guides/event-handler-in-pre-react-18.md similarity index 100% rename from docs/guides/event-handler-in-pre-react-18.md rename to old_docs/guides/event-handler-in-pre-react-18.md diff --git a/docs/guides/flux-inspired-practice.md b/old_docs/guides/flux-inspired-practice.md similarity index 100% rename from docs/guides/flux-inspired-practice.md rename to old_docs/guides/flux-inspired-practice.md diff --git a/docs/guides/how-to-reset-state.md b/old_docs/guides/how-to-reset-state.md similarity index 100% rename from docs/guides/how-to-reset-state.md rename to old_docs/guides/how-to-reset-state.md diff --git a/docs/guides/immutable-state-and-merging.md b/old_docs/guides/immutable-state-and-merging.md similarity index 100% rename from docs/guides/immutable-state-and-merging.md rename to old_docs/guides/immutable-state-and-merging.md diff --git a/docs/guides/initialize-state-with-props.md b/old_docs/guides/initialize-state-with-props.md similarity index 100% rename from docs/guides/initialize-state-with-props.md rename to old_docs/guides/initialize-state-with-props.md diff --git a/docs/guides/maps-and-sets-usage.md b/old_docs/guides/maps-and-sets-usage.md similarity index 100% rename from docs/guides/maps-and-sets-usage.md rename to old_docs/guides/maps-and-sets-usage.md diff --git a/docs/guides/nextjs.md b/old_docs/guides/nextjs.md similarity index 100% rename from docs/guides/nextjs.md rename to old_docs/guides/nextjs.md diff --git a/docs/guides/practice-with-no-store-actions.md b/old_docs/guides/practice-with-no-store-actions.md similarity index 100% rename from docs/guides/practice-with-no-store-actions.md rename to old_docs/guides/practice-with-no-store-actions.md diff --git a/docs/guides/prevent-rerenders-with-use-shallow.md b/old_docs/guides/prevent-rerenders-with-use-shallow.md similarity index 100% rename from docs/guides/prevent-rerenders-with-use-shallow.md rename to old_docs/guides/prevent-rerenders-with-use-shallow.md diff --git a/docs/guides/slices-pattern.md b/old_docs/guides/slices-pattern.md similarity index 100% rename from docs/guides/slices-pattern.md rename to old_docs/guides/slices-pattern.md diff --git a/docs/guides/ssr-and-hydration.md b/old_docs/guides/ssr-and-hydration.md similarity index 100% rename from docs/guides/ssr-and-hydration.md rename to old_docs/guides/ssr-and-hydration.md diff --git a/docs/guides/testing.md b/old_docs/guides/testing.md similarity index 100% rename from docs/guides/testing.md rename to old_docs/guides/testing.md diff --git a/docs/guides/typescript.md b/old_docs/guides/typescript.md similarity index 100% rename from docs/guides/typescript.md rename to old_docs/guides/typescript.md diff --git a/docs/guides/updating-state.md b/old_docs/guides/updating-state.md similarity index 100% rename from docs/guides/updating-state.md rename to old_docs/guides/updating-state.md diff --git a/docs/integrations/immer-middleware.md b/old_docs/integrations/immer-middleware.md similarity index 100% rename from docs/integrations/immer-middleware.md rename to old_docs/integrations/immer-middleware.md diff --git a/docs/integrations/persisting-store-data.md b/old_docs/integrations/persisting-store-data.md similarity index 100% rename from docs/integrations/persisting-store-data.md rename to old_docs/integrations/persisting-store-data.md diff --git a/docs/integrations/third-party-libraries.md b/old_docs/integrations/third-party-libraries.md similarity index 100% rename from docs/integrations/third-party-libraries.md rename to old_docs/integrations/third-party-libraries.md diff --git a/docs/migrations/migrating-to-v4.md b/old_docs/migrations/migrating-to-v4.md similarity index 100% rename from docs/migrations/migrating-to-v4.md rename to old_docs/migrations/migrating-to-v4.md diff --git a/docs/migrations/migrating-to-v5.md b/old_docs/migrations/migrating-to-v5.md similarity index 100% rename from docs/migrations/migrating-to-v5.md rename to old_docs/migrations/migrating-to-v5.md diff --git a/docs/previous-versions/zustand-v3-create-context.md b/old_docs/previous-versions/zustand-v3-create-context.md similarity index 100% rename from docs/previous-versions/zustand-v3-create-context.md rename to old_docs/previous-versions/zustand-v3-create-context.md