mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
Add docs for APIs and Hooks (#2706)
* Add docs for APIs and Hooks * Minor fixes * Minor fixes
This commit is contained in:
parent
4ec7077de5
commit
0a4f9d0f71
542
docs/apis/create-store.md
Normal file
542
docs/apis/create-store.md
Normal file
@ -0,0 +1,542 @@
|
||||
---
|
||||
title: createStore
|
||||
description: How to create vanilla stores
|
||||
nav: 202
|
||||
---
|
||||
|
||||
`createStore` lets you create a vanilla store that exposes API utilities.
|
||||
|
||||
```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<T>()(stateCreatorFn: StateCreator<T, [], []>): StoreApi<T>
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `stateCreatorFn`: A function that takes `set` function, `get` function and `api` as arguments.
|
||||
Usually, you will return an object with the methods you want to expose.
|
||||
|
||||
#### Returns
|
||||
|
||||
`createStore` returns a vanilla store that exposes API utilities, `setState`, `getState` and
|
||||
`subscribe`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Updating state based on previous state
|
||||
|
||||
This example shows 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<AgeStore>()((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 HTMLHeadingElement
|
||||
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<typeof ageStore.subscribe>[0] = (state) => {
|
||||
$yourAgeHeading.innerHTML = `Your age: ${state.age}`
|
||||
}
|
||||
|
||||
render(ageStore.getInitialState(), ageStore.getInitialState())
|
||||
|
||||
ageStore.subscribe(render)
|
||||
```
|
||||
|
||||
Here's the `html` code
|
||||
|
||||
```html
|
||||
<h1 id="your-age"></h1>
|
||||
<button id="increment-by-3" type="button">+3</button>
|
||||
<button id="increment-by-1" type="button">+1</button>
|
||||
```
|
||||
|
||||
### Updating Primitives in State
|
||||
|
||||
State can hold any kind of JavaScript value. When you want to update built-in primitive values like
|
||||
numbers, strings, booleans, etc. you 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<XStore>()(() => 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<typeof xStore.subscribe>[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
|
||||
<div
|
||||
id="dot-container"
|
||||
style="position: relative; width: 100vw; height: 100vh;"
|
||||
>
|
||||
<div
|
||||
id="dot"
|
||||
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const positionStore = create<PositionStore>()((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<typeof positionStore.subscribe>[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
|
||||
<div
|
||||
id="dot-container"
|
||||
style="position: relative; width: 100vw; height: 100vh;"
|
||||
>
|
||||
<div
|
||||
id="dot"
|
||||
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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: `[...array]`, `concat(...)`, `filter(...)`,
|
||||
> `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid
|
||||
> mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`,
|
||||
> `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`.
|
||||
|
||||
```ts
|
||||
import { create } from 'zustand'
|
||||
|
||||
type PositionStore = [number, number]
|
||||
|
||||
const positionStore = create<PositionStore>()(() => [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<typeof positionStore.subscribe>[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
|
||||
<div
|
||||
id="dot-container"
|
||||
style="position: relative; width: 100vw; height: 100vh;"
|
||||
>
|
||||
<div
|
||||
id="dot"
|
||||
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const positionStore = createStore<PositionStore>()((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<typeof positionStore.subscribe>[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<typeof positionStore.subscribe>[0] = (state) => {
|
||||
console.log('new position', { position: { x: state.x, y: state.x } })
|
||||
}
|
||||
|
||||
positionStore.subscribe(logger)
|
||||
```
|
||||
|
||||
Here's the `html` code
|
||||
|
||||
```html
|
||||
<div
|
||||
id="dot-container"
|
||||
style="position: relative; width: 100vw; height: 100vh;"
|
||||
>
|
||||
<div
|
||||
id="dot"
|
||||
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 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<PersonStoreState>) => void
|
||||
}
|
||||
|
||||
type PersonStore = PersonStoreState & PersonStoreActions
|
||||
|
||||
const personStore = createStore<PersonStore>()((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<typeof personStore.subscribe>[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
|
||||
<label style="display: block">
|
||||
First name:
|
||||
<input id="first-name" />
|
||||
</label>
|
||||
<label style="display: block">
|
||||
Last name:
|
||||
<input id="last-name" />
|
||||
</label>
|
||||
<label style="display: block">
|
||||
Email:
|
||||
<input id="email" />
|
||||
</label>
|
||||
<p id="result"></p>
|
||||
```
|
||||
|
||||
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 performing 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 {32-34,38-40,44-46}
|
||||
import { createStore } from 'zustand'
|
||||
|
||||
type PersonStoreState = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type PersonStoreActions = {
|
||||
setPerson: (nextPerson: Partial<PersonStoreState>) => void
|
||||
}
|
||||
|
||||
type PersonStore = PersonStoreState & PersonStoreActions
|
||||
|
||||
const personStore = createStore<PersonStore>()((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<typeof personStore.subscribe>[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)
|
||||
```
|
||||
620
docs/apis/create-with-equality-fn.md
Normal file
620
docs/apis/create-with-equality-fn.md
Normal file
@ -0,0 +1,620 @@
|
||||
---
|
||||
title: createWithEqualityFn ⚛️
|
||||
description: How to create efficient stores
|
||||
nav: 203
|
||||
---
|
||||
|
||||
`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<T>()(stateCreatorFn: StateCreator<T, [], []>, equalityFn?: (a: T, b: T) => boolean): UseBoundStore<StoreApi<T>>
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `stateCreatorFn`: A function that takes `set` function, `get` function and `api` as arguments.
|
||||
Usually, you will return an object with the methods you want to expose.
|
||||
- **optional** `equalityFn`: Defaults to `Object.is`. A function that lets you skip re-renders.
|
||||
|
||||
#### Returns
|
||||
|
||||
`createWithEqualityFn` returns a React Hook with some API utilities, `setState`, `getState`, and
|
||||
`subscribe`, attached like `create`. It lets you return data that is based on current state,
|
||||
using a selector function, and lets you skip re-renders using an 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 shows 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<AgeStore>()(
|
||||
(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 (
|
||||
<>
|
||||
<h1>Your age: {age}</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
increment()
|
||||
increment()
|
||||
increment()
|
||||
}}
|
||||
>
|
||||
+3
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
increment()
|
||||
}}
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Primitives in State
|
||||
|
||||
State can hold any kind of JavaScript value. When you want to update built-in primitive values like
|
||||
numbers, strings, booleans, etc. you 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<XStore>()(() => 0, shallow)
|
||||
|
||||
export default function MovingDot() {
|
||||
const x = useXStore()
|
||||
const setX = (nextX: number) => {
|
||||
useXStore.setState(nextX, true)
|
||||
}
|
||||
const position = { y: 0, x }
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setX(e.clientX)
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const usePositionStore = createWithEqualityFn<PositionStore>()(
|
||||
(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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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: `[...array]`, `concat(...)`, `filter(...)`,
|
||||
> `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid
|
||||
> mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`,
|
||||
> `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`.
|
||||
|
||||
```tsx
|
||||
import { createWithEqualityFn } from 'zustand/traditional'
|
||||
import { shallow } from 'zustand/vanilla/shallow'
|
||||
|
||||
type PositionStore = [number, number]
|
||||
|
||||
const usePositionStore = createWithEqualityFn<PositionStore>()(
|
||||
() => [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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition([e.clientX, e.clientY])
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
onMouseEnter={(event) => {
|
||||
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),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const usePositionStore = createWithEqualityFn<PositionStore>()(
|
||||
(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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
onMouseEnter={(event) => {
|
||||
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),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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<PersonStoreState>) => void
|
||||
}
|
||||
|
||||
type PersonStore = PersonStoreState & PersonStoreActions
|
||||
|
||||
const usePersonStore = createWithEqualityFn<PersonStore>()(
|
||||
(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 (
|
||||
<>
|
||||
<label style={{ display: 'block' }}>
|
||||
First name:
|
||||
<input value={person.firstName} onChange={handleFirstNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Last name:
|
||||
<input value={person.lastName} onChange={handleLastNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Email:
|
||||
<input value={person.email} onChange={handleEmailChange} />
|
||||
</label>
|
||||
<p>
|
||||
{person.firstName} {person.lastName} ({person.email})
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 performing 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 {39,43,47}
|
||||
import { createWithEqualityFn } from 'zustand/traditional'
|
||||
import { shallow } from 'zustand/vanilla/shallow'
|
||||
|
||||
type PersonStoreState = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type PersonStoreActions = {
|
||||
setPerson: (nextPerson: Partial<PersonStoreState>) => void
|
||||
}
|
||||
|
||||
type PersonStore = PersonStoreState & PersonStoreActions
|
||||
|
||||
const usePersonStore = createWithEqualityFn<PersonStore>()(
|
||||
(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 (
|
||||
<>
|
||||
<label style={{ display: 'block' }}>
|
||||
First name:
|
||||
<input value={person.firstName} onChange={handleFirstNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Last name:
|
||||
<input value={person.lastName} onChange={handleLastNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Email:
|
||||
<input value={person.email} onChange={handleEmailChange} />
|
||||
</label>
|
||||
<p>
|
||||
{person.firstName} {person.lastName} ({person.email})
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
590
docs/apis/create.md
Normal file
590
docs/apis/create.md
Normal file
@ -0,0 +1,590 @@
|
||||
---
|
||||
title: create ⚛️
|
||||
description: How to create stores
|
||||
nav: 204
|
||||
---
|
||||
|
||||
`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<T>()(stateCreatorFn: StateCreator<T, [], []>): UseBoundStore<StoreApi<T>>
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `stateCreatorFn`: A function that takes `set` function, `get` function and `api` as arguments.
|
||||
Usually, you will return an object with the methods you want to expose.
|
||||
|
||||
#### Returns
|
||||
|
||||
`create` returns a React Hook with some API utilities, `setState`, `getState`, and `subscribe`,
|
||||
attached. It lets you return data that is based on current state, using a 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 shows 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<AgeStore>()((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 (
|
||||
<>
|
||||
<h1>Your age: {age}</h1>
|
||||
<button
|
||||
onClick={() => {
|
||||
increment()
|
||||
increment()
|
||||
increment()
|
||||
}}
|
||||
>
|
||||
+3
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
increment()
|
||||
}}
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Primitives in State
|
||||
|
||||
State can hold any kind of JavaScript value. When you want to update built-in primitive values like
|
||||
numbers, strings, booleans, etc. you 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<XStore>()(() => 0)
|
||||
|
||||
export default function MovingDot() {
|
||||
const x = useXStore()
|
||||
const setX = (nextX: number) => {
|
||||
useXStore.setState(nextX, true)
|
||||
}
|
||||
const position = { y: 0, x }
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setX(e.clientX)
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const usePositionStore = create<PositionStore>()((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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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: `[...array]`, `concat(...)`, `filter(...)`,
|
||||
> `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid
|
||||
> mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`,
|
||||
> `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`.
|
||||
|
||||
```tsx
|
||||
import { create } from 'zustand'
|
||||
|
||||
type PositionStore = [number, number]
|
||||
|
||||
const usePositionStore = create<PositionStore>()(() => [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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition([e.clientX, e.clientY])
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
onMouseEnter={(event) => {
|
||||
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),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const usePositionStore = create<PositionStore>()((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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
onMouseEnter={(event) => {
|
||||
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),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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<PersonStoreState>) => void
|
||||
}
|
||||
|
||||
type PersonStore = PersonStoreState & PersonStoreActions
|
||||
|
||||
const usePersonStore = create<PersonStore>()((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 (
|
||||
<>
|
||||
<label style={{ display: 'block' }}>
|
||||
First name:
|
||||
<input value={person.firstName} onChange={handleFirstNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Last name:
|
||||
<input value={person.lastName} onChange={handleLastNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Email:
|
||||
<input value={person.email} onChange={handleEmailChange} />
|
||||
</label>
|
||||
<p>
|
||||
{person.firstName} {person.lastName} ({person.email})
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 performing 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 {35,39,43}
|
||||
import { create } from 'zustand'
|
||||
|
||||
type PersonStoreState = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type PersonStoreActions = {
|
||||
setPerson: (nextPerson: Partial<PersonStoreState>) => void
|
||||
}
|
||||
|
||||
type PersonStore = PersonStoreState & PersonStoreActions
|
||||
|
||||
const usePersonStore = create<PersonStore>()((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 (
|
||||
<>
|
||||
<label style={{ display: 'block' }}>
|
||||
First name:
|
||||
<input value={person.firstName} onChange={handleFirstNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Last name:
|
||||
<input value={person.lastName} onChange={handleLastNameChange} />
|
||||
</label>
|
||||
<label style={{ display: 'block' }}>
|
||||
Email:
|
||||
<input value={person.email} onChange={handleEmailChange} />
|
||||
</label>
|
||||
<p>
|
||||
{person.firstName} {person.lastName} ({person.email})
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
221
docs/apis/shallow.md
Normal file
221
docs/apis/shallow.md
Normal file
@ -0,0 +1,221 @@
|
||||
---
|
||||
title: shallow
|
||||
description: How compare simple data effectively
|
||||
nav: 209
|
||||
---
|
||||
|
||||
`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<T>(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.
|
||||
262
docs/hooks/use-shallow.md
Normal file
262
docs/hooks/use-shallow.md
Normal file
@ -0,0 +1,262 @@
|
||||
---
|
||||
title: useShallow ⚛️
|
||||
description: How to memoize selector functions
|
||||
nav: 211
|
||||
---
|
||||
|
||||
`useShallow` is a React Hook that lets you optimize re-renders.
|
||||
|
||||
```js
|
||||
useShallow(selector)
|
||||
```
|
||||
|
||||
- [Reference](#reference)
|
||||
- [Signature](#useshallow-signature)
|
||||
- [Usage](#usage)
|
||||
- [Writing a memoized selector](#writing-a-memoized-selector)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- TBD
|
||||
|
||||
## Reference
|
||||
|
||||
### `useShallow` Signature
|
||||
|
||||
```ts
|
||||
useShallow<T, U = T>(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<BearFamilyMealsStore>()(() => ({
|
||||
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 <div>{names.join(', ')}</div>
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<>
|
||||
<UpdateTinyBearPorridge />
|
||||
<BearNames />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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<BearFamilyMealsStore>()(() => ({
|
||||
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 <div>{names.join(', ')}</div>
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<UpdateBabyBearMeal />
|
||||
<BearNames />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 <div>{names.join(', ')}</div>
|
||||
}
|
||||
```
|
||||
|
||||
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<BearFamilyMealsStore>()(() => ({
|
||||
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 <div>{names.join(', ')}</div>
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<UpdateBabyBearMeal />
|
||||
<BearNames />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
By using `useShallow`, we optimized the rendering process, ensuring that the component only
|
||||
re-renders when necessary, which improves overall performance.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
TBD
|
||||
958
docs/hooks/use-store-with-equality-fn.md
Normal file
958
docs/hooks/use-store-with-equality-fn.md
Normal file
@ -0,0 +1,958 @@
|
||||
---
|
||||
title: useStoreWithEqualityFn ⚛️
|
||||
description: How to use vanilla stores effectively in React
|
||||
nav: 212
|
||||
---
|
||||
|
||||
`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<T, U = T>(storeApi: StoreApi<T>, 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 any data based on current state depending on the selector function,
|
||||
and lets you skip re-renders using an equality function. It should take a store, a selector
|
||||
function, and an equality function as arguments.
|
||||
|
||||
## 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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const positionStore = createStore<PositionStore>()((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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we’ll render the `MovingDot` component in our `App` component.
|
||||
|
||||
```tsx
|
||||
export default function App() {
|
||||
return <MovingDot />
|
||||
}
|
||||
```
|
||||
|
||||
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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const positionStore = createStore<PositionStore>()((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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <MovingDot />
|
||||
}
|
||||
```
|
||||
|
||||
### 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<CounterStore>()((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<typeof createCounterStore>
|
||||
>()
|
||||
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
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 <Tabs />
|
||||
}
|
||||
```
|
||||
|
||||
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<CounterStore>()((set) => ({
|
||||
count: 0,
|
||||
increment: () => {
|
||||
set((state) => ({ count: state.count + 1 }))
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const defaultCounterStores = new Map<
|
||||
string,
|
||||
ReturnType<typeof createCounterStore>
|
||||
>()
|
||||
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const createPositionStore = () => {
|
||||
return createStore<PositionStore>()((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<ReturnType<
|
||||
typeof createPositionStore
|
||||
> | null>(null)
|
||||
|
||||
function PositionStoreProvider({ children }: { children: ReactNode }) {
|
||||
const [positionStore] = useState(createPositionStore)
|
||||
|
||||
return (
|
||||
<PositionStoreContext.Provider value={positionStore}>
|
||||
{children}
|
||||
</PositionStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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<U>(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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x:
|
||||
e.clientX > e.currentTarget.clientWidth
|
||||
? e.clientX - e.currentTarget.clientWidth
|
||||
: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '50vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: color,
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="red" />
|
||||
</PositionStoreProvider>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="blue" />
|
||||
</PositionStoreProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const createPositionStore = () => {
|
||||
return createStore<PositionStore>()((set) => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
setPosition: (nextPosition) => {
|
||||
set(nextPosition)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const PositionStoreContext = createContext<ReturnType<
|
||||
typeof createPositionStore
|
||||
> | null>(null)
|
||||
|
||||
function PositionStoreProvider({ children }: { children: ReactNode }) {
|
||||
const [positionStore] = useState(createPositionStore)
|
||||
|
||||
return (
|
||||
<PositionStoreContext.Provider value={positionStore}>
|
||||
{children}
|
||||
</PositionStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function usePositionStore<U>(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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x:
|
||||
e.clientX > e.currentTarget.clientWidth
|
||||
? e.clientX - e.currentTarget.clientWidth
|
||||
: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '50vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: color,
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="red" />
|
||||
</PositionStoreProvider>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="blue" />
|
||||
</PositionStoreProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<CounterStore>()((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<string, ReturnType<typeof createCounterStore>>,
|
||||
) => {
|
||||
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<string, ReturnType<typeof createCounterStore>>(),
|
||||
)
|
||||
|
||||
return (
|
||||
<CounterStoresContext.Provider>{children}</CounterStoresContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a
|
||||
given tab.
|
||||
|
||||
```tsx
|
||||
const useCounterStore = <U,>(
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<CounterStoresProvider>
|
||||
<Tabs />
|
||||
</CounterStoresProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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<CounterStore>()((set) => ({
|
||||
count: 0,
|
||||
increment: () => {
|
||||
set((state) => ({ count: state.count + 1 }))
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const createCounterStoreFactory = (
|
||||
counterStores: Map<string, ReturnType<typeof createCounterStore>>,
|
||||
) => {
|
||||
return (counterStoreKey: string) => {
|
||||
if (!counterStores.has(counterStoreKey)) {
|
||||
counterStores.set(counterStoreKey, createCounterStore())
|
||||
}
|
||||
return counterStores.get(counterStoreKey)!
|
||||
}
|
||||
}
|
||||
|
||||
const CounterStoresContext = createContext<Map<
|
||||
string,
|
||||
ReturnType<typeof createCounterStore>
|
||||
> | null>(null)
|
||||
|
||||
const CounterStoresProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [stores] = useState(
|
||||
() => new Map<string, ReturnType<typeof createCounterStore>>(),
|
||||
)
|
||||
|
||||
return (
|
||||
<CounterStoresContext.Provider value={stores}>
|
||||
{children}
|
||||
</CounterStoresContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useCounterStore = <U,>(
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<CounterStoresProvider>
|
||||
<Tabs />
|
||||
</CounterStoresProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
TBD
|
||||
936
docs/hooks/use-store.md
Normal file
936
docs/hooks/use-store.md
Normal file
@ -0,0 +1,936 @@
|
||||
---
|
||||
title: useStore ⚛️
|
||||
description: How to use vanilla stores in React
|
||||
nav: 213
|
||||
---
|
||||
|
||||
`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<StoreApi<T>, U = T>(storeApi: StoreApi<T>, selectorFn?: (state: T) => U) => UseBoundStore<StoreApi<T>>
|
||||
```
|
||||
|
||||
#### 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 any data based on current state depending on the selector function. It should
|
||||
take a store, and a selector function as arguments.
|
||||
|
||||
## 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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const positionStore = createStore<PositionStore>()((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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we’ll render the `MovingDot` component in our `App` component.
|
||||
|
||||
```tsx
|
||||
export default function App() {
|
||||
return <MovingDot />
|
||||
}
|
||||
```
|
||||
|
||||
Here is what the code should look like:
|
||||
|
||||
```tsx
|
||||
import { createStore, useStore } from 'zustand'
|
||||
|
||||
type PositionStoreState = { x: number; y: number }
|
||||
|
||||
type PositionStoreActions = {
|
||||
setPosition: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const positionStore = createStore<PositionStore>()((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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'red',
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <MovingDot />
|
||||
}
|
||||
```
|
||||
|
||||
### 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<CounterStore>()((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<typeof createCounterStore>
|
||||
>()
|
||||
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
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 <Tabs />
|
||||
}
|
||||
```
|
||||
|
||||
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<CounterStore>()((set) => ({
|
||||
count: 0,
|
||||
increment: () => {
|
||||
set((state) => ({ count: state.count + 1 }))
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const defaultCounterStores = new Map<
|
||||
string,
|
||||
ReturnType<typeof createCounterStore>
|
||||
>()
|
||||
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const createPositionStore = () => {
|
||||
return createStore<PositionStore>()((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<ReturnType<
|
||||
typeof createPositionStore
|
||||
> | null>(null)
|
||||
|
||||
function PositionStoreProvider({ children }: { children: ReactNode }) {
|
||||
const [positionStore] = useState(createPositionStore)
|
||||
|
||||
return (
|
||||
<PositionStoreContext.Provider value={positionStore}>
|
||||
{children}
|
||||
</PositionStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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<U>(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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x:
|
||||
e.clientX > e.currentTarget.clientWidth
|
||||
? e.clientX - e.currentTarget.clientWidth
|
||||
: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '50vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: color,
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="red" />
|
||||
</PositionStoreProvider>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="blue" />
|
||||
</PositionStoreProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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: (nextPosition: Partial<PositionStoreState>) => void
|
||||
}
|
||||
|
||||
type PositionStore = PositionStoreState & PositionStoreActions
|
||||
|
||||
const createPositionStore = () => {
|
||||
return createStore<PositionStore>()((set) => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
setPosition: (nextPosition) => {
|
||||
set(nextPosition)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const PositionStoreContext = createContext<ReturnType<
|
||||
typeof createPositionStore
|
||||
> | null>(null)
|
||||
|
||||
function PositionStoreProvider({ children }: { children: ReactNode }) {
|
||||
const [positionStore] = useState(createPositionStore)
|
||||
|
||||
return (
|
||||
<PositionStoreContext.Provider value={positionStore}>
|
||||
{children}
|
||||
</PositionStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function usePositionStore<U>(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 (
|
||||
<div
|
||||
onPointerMove={(e) => {
|
||||
setPosition({
|
||||
x:
|
||||
e.clientX > e.currentTarget.clientWidth
|
||||
? e.clientX - e.currentTarget.clientWidth
|
||||
: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '50vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: color,
|
||||
borderRadius: '50%',
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
left: -10,
|
||||
top: -10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="red" />
|
||||
</PositionStoreProvider>
|
||||
<PositionStoreProvider>
|
||||
<MovingDot color="blue" />
|
||||
</PositionStoreProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<CounterStore>()((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<string, ReturnType<typeof createCounterStore>>,
|
||||
) => {
|
||||
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<string, ReturnType<typeof createCounterStore>>(),
|
||||
)
|
||||
|
||||
return (
|
||||
<CounterStoresContext.Provider>{children}</CounterStoresContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a
|
||||
given tab.
|
||||
|
||||
```tsx
|
||||
const useCounterStore = <U>(
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<CounterStoresProvider>
|
||||
<Tabs />
|
||||
</CounterStoresProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
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<CounterStore>()((set) => ({
|
||||
count: 0,
|
||||
increment: () => {
|
||||
set((state) => ({ count: state.count + 1 }))
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const createCounterStoreFactory = (
|
||||
counterStores: Map<string, ReturnType<typeof createCounterStore>>,
|
||||
) => {
|
||||
return (counterStoreKey: string) => {
|
||||
if (!counterStores.has(counterStoreKey)) {
|
||||
counterStores.set(counterStoreKey, createCounterStore())
|
||||
}
|
||||
return counterStores.get(counterStoreKey)!
|
||||
}
|
||||
}
|
||||
|
||||
const CounterStoresContext = createContext<Map<
|
||||
string,
|
||||
ReturnType<typeof createCounterStore>
|
||||
> | null>(null)
|
||||
|
||||
const CounterStoresProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [stores] = useState(
|
||||
() => new Map<string, ReturnType<typeof createCounterStore>>(),
|
||||
)
|
||||
|
||||
return (
|
||||
<CounterStoresContext.Provider value={stores}>
|
||||
{children}
|
||||
</CounterStoresContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useCounterStore = <U,>(
|
||||
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 (
|
||||
<div style={{ fontFamily: 'monospace' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
borderBottom: '1px solid salmon',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(0)}
|
||||
>
|
||||
Tab 1
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(1)}
|
||||
>
|
||||
Tab 2
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
border: '1px solid salmon',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setCurrentTabIndex(2)}
|
||||
>
|
||||
Tab 3
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 4 }}>
|
||||
Content of Tab {currentTabIndex + 1}
|
||||
<br /> <br />
|
||||
<button type="button" onClick={() => counterState.increment()}>
|
||||
Count: {counterState.count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<CounterStoresProvider>
|
||||
<Tabs />
|
||||
</CounterStoresProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
TBD
|
||||
Loading…
x
Reference in New Issue
Block a user