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