create react
create 让你创建一个附带 API 工具的 React Hook。
const useSomeStore = create(stateCreatorFn)
类型
签名
create<T>()(stateCreatorFn: StateCreator<T, [], []>): UseBoundStore<StoreApi<T>>
参考
create(stateCreatorFn)
参数
stateCreatorFn:一个接收 set 函数、get 函数和 store 作为参数的函数。
通常,你会返回一个对象,其中包含你希望暴露的方法。
CAUTION
在初始化期间不要同步调用 set、get 和 store——只有在 store 创建完成之后调用它们
才是安全的(例如在 action 方法内部调用)。
返回值
create 返回一个 React Hook,并附带 setState、getState、getInitialState 和
subscribe 等 API 工具。借助选择器函数,它可以让你返回基于当前状态的数据。
这个 Hook 应该只接收一个选择器函数作为参数。
用法
基于先前的状态更新状态
要基于先前的状态更新状态,我们应该使用更新函数(updater functions)。可以在
此处了解更多。
这个示例展示了如何在 action 中支持更新函数。
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 = useAgeStore((state) => state.age)
const setAge = useAgeStore((state) => 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>
</>
)
}
更新状态中的原始值
状态可以保存任意类型的 JavaScript 值。当你想要更新数字、字符串、布尔值等内置原始值时,
应当直接赋新值,以确保更新被正确应用,并避免出现意料之外的行为。
NOTE
默认情况下,set 函数执行浅合并。如果你需要用一个新的状态完全替换原有状态,请把
replace 参数设为 true
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>
)
}
更新状态中的对象
在 JavaScript 中对象是可变的,但当你要把它们保存在状态中时,应当把它们当作不可变的。
相反,当你想要更新一个对象时,需要创建一个新对象(或复制现有对象),然后把状态设置为使用这个新对象。
默认情况下,set 函数执行浅合并。对于大多数只需修改特定属性的更新而言,默认的浅合并
更高效,因此是首选。若要用新状态完全替换原有状态,请谨慎地把 replace 参数设为 true,
因为它会丢弃状态中已有的所有嵌套数据。
import { create } from 'zustand'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const usePositionStore = create<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (nextPosition) => set({ position: nextPosition }),
}))
export default function MovingDot() {
const position = usePositionStore((state) => state.position)
const setPosition = usePositionStore((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>
)
}
更新状态中的数组
在 JavaScript 中数组是可变的,但当你要把它们保存在状态中时,应当把它们当作不可变的。
与对象一样,当你想要更新保存在状态中的数组时,需要创建一个新数组(或复制现有数组),
然后把状态设置为使用这个新数组。
默认情况下,set 函数执行浅合并。要更新数组中的值,我们应当赋新值,以确保更新被正确
应用,并避免出现意料之外的行为。若要用新状态完全替换原有状态,请把 replace 参数设为 true。
CAUTION
我们应当优先使用不可变操作,例如:[...array]、concat(...)、filter(...)、
slice(...)、map(...)、toSpliced(...)、toSorted(...) 和 toReversed(...),并避免
使用可变操作,例如 array[arrayIndex] = ...、push(...)、unshift(...)、pop(...)、
shift(...)、splice(...)、reverse(...) 和 sort(...)。
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>
)
}
在没有 store action 的情况下更新状态
在模块层级、store 之外定义 action 有几个好处:调用 action 时不需要 Hook,而且便于代码分割。
NOTE
推荐的做法是把 action 和状态放在同一个 store 中(让 action 与状态放在一起)。
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>
)
}
订阅状态更新
通过订阅状态更新,你可以注册一个回调,每当 store 的状态更新时它就会被触发。
我们可以使用 subscribe 来做外部状态管理。
import { useEffect } from 'react'
import { create } from 'zustand'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const usePositionStore = create<PositionStore>()((set) => ({
position: { x: 0, y: 0 },
setPosition: (nextPosition) => set({ position: nextPosition }),
}))
export default function MovingDot() {
const position = usePositionStore((state) => state.position)
const setPosition = usePositionStore((state) => state.setPosition)
useEffect(() => {
const unsubscribePositionStore = usePositionStore.subscribe(({ position }) => {
console.log('new position', { position })
})
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>
)
}
疑难解答
我更新了状态,但屏幕没有更新
在上一个示例中,position 对象总是根据当前光标位置新建的。但通常情况下,你会希望把已有数据
作为正在创建的新对象的一部分。例如,你可能只想更新表单中的一个字段,而保持其他所有字段的
先前值不变。
这些输入框无法正常工作,因为 onChange 处理函数修改了状态:
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: '[email protected]',
setPerson: (nextPerson) => set(nextPerson),
}))
export default function Form() {
const person = usePersonStore((state) => state)
const setPerson = usePersonStore((state) => state.setPerson)
function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) {
person.firstName = e.target.value
}
function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) {
person.lastName = e.target.value
}
function handleEmailChange(e: ChangeEvent<HTMLInputElement>) {
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>
</>
)
}
例如,下面这行代码修改了来自上一次渲染的状态:
person.firstName = e.target.value
要想可靠地获得你想要的行为,应当创建一个新对象并传给 setPerson。但这里你还需要把已有数据
复制进去,因为只有一个字段发生了变化:
setPerson({ ...person, firstName: e.target.value }) // New first name from the input
NOTE
由于 set 函数默认执行浅合并,我们不需要逐个复制每个属性。
现在表单可以正常工作了!
注意,你没有为每个输入框单独声明一个状态变量。对于大型表单,把所有数据集中保存在一个对象中
非常方便——只要你正确地更新它!
import { create } from 'zustand'
type PersonStoreState = {
person: { firstName: string; lastName: string; email: string }
}
type PersonStoreActions = {
setPerson: (nextPerson: PersonStoreState['person']) => void
}
type PersonStore = PersonStoreState & PersonStoreActions
const usePersonStore = create<PersonStore>()((set) => ({
person: {
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]',
},
setPerson: (nextPerson) => set(nextPerson),
}))
export default function Form() {
const person = usePersonStore((state) => state.person)
const setPerson = usePersonStore((state) => state.setPerson)
function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) {
setPerson({ ...person, firstName: e.target.value })
}
function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) {
setPerson({ ...person, lastName: e.target.value })
}
function handleEmailChange(e: ChangeEvent<HTMLInputElement>) {
setPerson({ ...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>
</>
)
}