createStore
createStore 让你创建一个原生 store,它对外暴露 API 工具函数。
const someStore = createStore(stateCreatorFn)
类型
签名
createStore<T>()(stateCreatorFn: StateCreator<T, [], []>): StoreApi<T>
参考
createStore(stateCreatorFn)
参数
stateCreatorFn:一个接收 set 函数、get 函数和 store 作为参数的函数。
通常,你会返回一个对象,其中包含你想要暴露的方法。
CAUTION
不要在初始化期间同步调用 set、get 和 store——只有在 store 创建完成之后调用才是安全的
(例如在 action 方法内部调用)。
返回值
createStore 返回一个原生 store,它对外暴露 API 工具:setState、getState、
getInitialState 和 subscribe。
用法
基于之前的状态更新状态
这个示例展示了如何在 action 中支持 更新函数(updater functions)。
import { createStore } from 'zustand/vanilla'
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)
以下是 html 代码
<h1 id="your-age"></h1>
<button id="increment-by-3" type="button">+3</button>
<button id="increment-by-1" type="button">+1</button>
更新状态中的原始值
状态可以保存任何类型的 JavaScript 值。当你要更新数字、字符串、布尔值等内置原始值时,
应当直接赋新值,以确保更新被正确应用,并避免意外行为。
NOTE
默认情况下,set 函数执行浅合并。如果你需要用一个新的状态完全替换原有状态,
请把 replace 参数设为 true。
import { createStore } from 'zustand/vanilla'
type XStore = number
const xStore = createStore<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] = (x) => {
$dot.style.transform = `translate(${x}px, 0)`
}
render(xStore.getInitialState(), xStore.getInitialState())
xStore.subscribe(render)
以下是 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>
更新状态中的对象
在 JavaScript 中对象是可变的,但把对象存进状态时,你应当把它们当作不可变的。
相反,更新对象时,你需要创建一个新对象(或复制一个已有对象),然后把状态设置为使用这个新对象。
默认情况下,set 函数执行浅合并。对于大多数只需要修改特定属性的更新来说,更推荐默认的
浅合并,因为它更高效。要用一个新状态完全替换原有状态时,请谨慎地把 replace 参数设为
true,因为这会丢弃状态中已有的任何嵌套数据。
import { createStore } from 'zustand/vanilla'
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 }),
}))
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) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getInitialState(), positionStore.getInitialState())
positionStore.subscribe(render)
以下是 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>
更新状态中的数组
在 JavaScript 中数组是可变的,但把数组存进状态时,你应当把它们当作不可变的。
和对象一样,更新状态中保存的数组时,你需要创建一个新数组(或复制一个已有数组),
然后把状态设置为使用这个新数组。
默认情况下,set 函数执行浅合并。更新数组值时,我们应当赋新值,以确保更新被正确应用,
并避免意外行为。要用一个新状态完全替换原有状态时,请把 replace 参数设为 true。
CAUTION
我们应当优先使用不可变操作,例如 [...array]、concat(...)、filter(...)、
slice(...)、map(...)、toSpliced(...)、toSorted(...) 和 toReversed(...),
并避免可变操作,例如 array[arrayIndex] = ...、push(...)、unshift(...)、pop(...)、
shift(...)、splice(...)、reverse(...) 和 sort(...)。
import { createStore } from 'zustand/vanilla'
type PositionStore = [number, number]
const positionStore = createStore<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] = ([x, y]) => {
$dot.style.transform = `translate(${x}px, ${y}px)`
}
render(positionStore.getInitialState(), positionStore.getInitialState())
positionStore.subscribe(render)
以下是 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>
订阅状态更新
通过订阅状态更新,你可以注册一个回调,每当 store 的状态更新时它就会触发。我们可以用
subscribe 来做外部状态管理。
import { createStore } from 'zustand/vanilla'
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 }),
}))
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) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getInitialState(), positionStore.getInitialState())
positionStore.subscribe(render)
const logger: Parameters<typeof positionStore.subscribe>[0] = (state) => {
console.log('new position', { position: state.position })
}
positionStore.subscribe(logger)
以下是 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>
疑难解答
我已经更新了状态,但屏幕没有更新
在上一个示例中,position 对象总是根据当前光标位置新建的。但很多时候,你会希望把已有数据
并入你正在创建的新对象。例如,你可能只想更新表单中的一个字段,而让其他所有字段保留之前的值。
这些输入框不起作用,因为 oninput 处理函数直接修改了状态:
import { createStore } from 'zustand/vanilla'
type PersonStoreState = {
person: { firstName: string; lastName: string; email: string }
}
type PersonStoreActions = {
setPerson: (nextPerson: PersonStoreState['person']) => void
}
type PersonStore = PersonStoreState & PersonStoreActions
const personStore = createStore<PersonStore>()((set) => ({
person: {
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]',
},
setPerson: (person) => set({ person }),
}))
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().person.firstName = (event.target as any).value
}
function handleLastNameChange(event: Event) {
personStore.getState().person.lastName = (event.target as any).value
}
function handleEmailChange(event: Event) {
personStore.getState().person.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) => {
$firstNameInput.value = state.person.firstName
$lastNameInput.value = state.person.lastName
$emailInput.value = state.person.email
$result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})`
}
render(personStore.getInitialState(), personStore.getInitialState())
personStore.subscribe(render)
以下是 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>
例如,这一行修改的是上一次渲染时的状态:
personStore.getState().firstName = (e.target as any).value
要可靠地得到你想要的行为,应当创建一个新对象并把它传给 setPerson。但这里你还想把已有数据
一并复制进去,因为只有其中一个字段发生了变化:
personStore.getState().setPerson({
firstName: e.target.value, // New first name from the input
})
NOTE
由于 set 函数默认执行浅合并,我们不需要逐个复制每个属性。
现在表单可以正常工作了!
注意,你没有为每个输入框单独声明状态变量。对于大型表单,把所有数据集中放在一个对象里
非常方便——只要你以正确的方式更新它!
import { createStore } from 'zustand/vanilla'
type PersonStoreState = {
person: { firstName: string; lastName: string; email: string }
}
type PersonStoreActions = {
setPerson: (nextPerson: PersonStoreState['person']) => void
}
type PersonStore = PersonStoreState & PersonStoreActions
const personStore = createStore<PersonStore>()((set) => ({
person: {
firstName: 'Barbara',
lastName: 'Hepworth',
email: '[email protected]',
},
setPerson: (person) => set({ person }),
}))
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({
...personStore.getState().person,
firstName: (event.target as any).value,
})
}
function handleLastNameChange(event: Event) {
personStore.getState().setPerson({
...personStore.getState().person,
lastName: (event.target as any).value,
})
}
function handleEmailChange(event: Event) {
personStore.getState().setPerson({
...personStore.getState().person,
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) => {
$firstNameInput.value = state.person.firstName
$lastNameInput.value = state.person.lastName
$emailInput.value = state.person.email
$result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})`
}
render(personStore.getInitialState(), personStore.getInitialState())
personStore.subscribe(render)