zustand/docs/hooks/use-store.md
Prince Raj f9b5538402
Update use-store.md (#2799)
there is a typo mistake while importing the setposition from positionstore

 wrong : const setPosition = useStore(positionStore, (state) => state.setPositionStore)
correct :  const setPosition = useStore(positionStore, (state) => state.setPosition)
2024-10-18 20:55:13 +09:00

923 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: useStore ⚛️
description: How to use vanilla stores in React
nav: 30
---
`useStore` is a React Hook that lets you use a vanilla store in React.
```js
const someState = useStore(store, selectorFn)
```
- [Types](#types)
- [Signature](#signature)
- [Reference](#reference)
- [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)
## Types
### Signature
```ts
useStore<StoreApi<T>, U = T>(store: StoreApi<T>, selectorFn?: (state: T) => U) => UseBoundStore<StoreApi<T>>
```
## Reference
### `useStore(store, selectorFn)`
#### 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 = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
```
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 = useStore(positionStore, (state) => state.position)
const setPosition = useStore(positionStore, (state) => 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 = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
function MovingDot() {
const position = useStore(positionStore, (state) => state.position)
const setPosition = useStore(positionStore, (state) => 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 = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const createPositionStore = () => {
return createStore<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
}
```
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 = usePositionStore((state) => state.position)
const setPosition = usePositionStore((state) => state.setPosition)
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 = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const createPositionStore = () => {
return createStore<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
}
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 = usePositionStore((state) => state.position)
const setPosition = usePositionStore((state) => state.setPosition)
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