• 简体中文
  • 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)