Add docs for APIs and Hooks (#2706)

* Add docs for APIs and Hooks

* Minor fixes

* Minor fixes
This commit is contained in:
Danilo Britto 2024-09-06 12:26:16 -05:00 committed by GitHub
parent 4ec7077de5
commit 0a4f9d0f71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 4129 additions and 0 deletions

542
docs/apis/create-store.md Normal file
View 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)
- [Ive updated the state, but the screen doesnt 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
### Ive updated the state, but the screen doesnt 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 youre
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 dont 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 youre 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 dont need to copy every property separately due to `set` function performing shallow merge by
> default.
Now the form works!
Notice how you didnt 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)
```

View 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)
- [Ive updated the state, but the screen doesnt 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
### Ive updated the state, but the screen doesnt 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 youre
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 dont 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 youre 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 dont need to copy every property separately due to `set` function performing shallow merge by
> default.
Now the form works!
Notice how you didnt 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
View 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)
- [Ive updated the state, but the screen doesnt 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
### Ive updated the state, but the screen doesnt 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 youre
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 dont 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 youre 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 dont need to copy every property separately due to `set` function performing shallow merge by
> default.
Now the form works!
Notice how you didnt 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
View 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
View 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 theres a small problem: the `BearNames` component keeps
re-rendering even if the names havent 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) hasnt 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

View 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, well 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, lets build the Tabs component, where users can switch between tabs and increment each tabs
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, well 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. Well use Reacts 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, well 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, lets build the Tabs component, where users can switch between tabs and increment each tabs
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
View 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, well 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, lets build the Tabs component, where users can switch between tabs and increment each tabs
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, well 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. Well use Reacts 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, well 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, lets build the Tabs component, where users can switch between tabs and increment each tabs
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