Zustand
1.序:
在 React 中,“状态管理”是指管理组件之间共享的数据状态的方式。简单来说,就是:
让组件知道数据的来源,并在数据发生变化时正确地更新 UI。
但是,如果组件结构非常的复杂,单靠useState和props就不够用了。因此,我们需要一些额外的工具来帮助我们管理状态,比如Zustand。
2.为什么要用Zustand:
很简单,因为它简单好用😋:
2.1 **简单的API **:
Zustand 只需要一个 create
函数来创建状态,不需要 reducer、action、dispatch、middleware 等复杂概念。
1 2 3 4 5 6 7 8
| import { create } from 'zustand';
const useStore = create((set) => ({ count: 0, increase: () => set((state) => ({ count: state.count + 1 })), }));
export default useStore
|
调用也非常简单:
1 2
| const count = useStore((state) => state.count); const increase = useStore((state) => state.increase);
|
如果要在别的文件中使用,也不需要什么createContext,useContext之类的东西了,只需import导入即可
1
| import useStore from './store'
|
2.2 简单的共享:
不需要使用<createContext.Provider>来包裹,store全局管理状态,因此可以直接访问,而且还有各种不同的读取方法:
2.2.1 全局读取:.getState()
zustand 在创建 store 时,会在返回的 hook 上附带一个静态方法**.getState(),用于在 React 组件之外**,即时读取当前状态快照
注:使用 useStore.getState()
获取的状态不会触发组件的重新渲染。 如果你在组件中使用它来读取状态,即使状态发生变化,组件也不会自动更新。
1 2 3 4 5
| import useStore from './store';
const currentCount = useStore.getState().count; console.log(currentCount);
|
2.2.2 响应式组件:useStore(selector)
在 React 组件内,直接调用 useStore(selector)
,zustand 只会让使用到特定状态切片的组件发生重新渲染:
1
| const count = useStore(state => state.count);
|
2.2.3 订阅状态变化的监听器:subscribe
zustand 还提供了subscribe,允许在组件外或模块中监听状态变化,适用于需要在状态变化时执行特定逻辑的场景,如日志记录等。
1 2 3 4 5 6 7
| const unsub1 = useStore.subscribe(console.log);
const unsub2 = useStore.subscribe((state) => state.count, (count) => { console.log('count变化了:', count); });
|
2.3 简单的异步:
相对于redux原生不支持异步操作,需要导入中间件(redux-thunk),zustand直接写就对了:
1 2 3 4 5 6 7
| const useStore = create((set) => ({ kfcvwo50: null, fetchUser: async () => { const res = await axios.get('/Thursday'); set({ kfcvwo50: res }); }, }));
|
2.4 简单的渲染:
react原生的useState在更新state时,所有使用该状态的组件都会重新渲染,哪怕组件没有实际用到改变的部分。但是zustand只有依赖特定状态的组件会重新渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import React, { useState, useContext, createContext } from 'react';
const AppContext = createContext();
const CounterProvider = ({ children }) => { const [count, setCount] = useState(0); const [message, setMessage] = useState('Hello');
return ( <AppContext.Provider value={{ count, setCount, message, setMessage }}> {children} </AppContext.Provider> ); };
const ComponentA = () => { const { count } = useContext(AppContext); return <div>Component A: {count}</div>; };
const ComponentB = () => { const { message } = useContext(AppContext); return <div>Component B: {message}</div>; };
const App = () => { const { setCount } = useContext(AppContext);
return ( <CounterProvider> <button onClick={() => setCount((prev) => prev + 1)}>Increment Count</button> <ComponentA /> <ComponentB /> </CounterProvider> ); };
export default App;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import React from 'react'; import create from 'zustand';
const useStore = create((set) => ({ count: 0, message: 'Hello', incrementCount: () => set((state) => ({ count: state.count + 1 })), }));
const ComponentA = () => { const count = useStore((state) => state.count); return <div>Component A: {count}</div>; };
const ComponentB = () => { const message = useStore((state) => state.message); return <div>Component B: {message}</div>; };
const App = () => { const incrementCount = useStore((state) => state.incrementCount);
return ( <> <button onClick={incrementCount}>Increment Count</button> <ComponentA /> <ComponentB /> </> ); };
export default App;
|
除了上面的外,zustand还有其他优点,比如轻量级,易于集成之类的。那么,这么好用的东西怎么用呢?别急,你很急吗😡。
3.zustand的安装和使用:
安装zustand:
第一步,创建一个新的React应用并且安装 Zustand
依赖, 运行下的的命令:
省流版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { create } from 'zustand'
const useStore = create(() => ({ who: '奶龙', }))
function App() { const who = useStore(state => state.who)
return ( <> Hello {who}! </> ) }
export default App
|
创建并访问store:
首先,我们从 zustand 的 create() API 中传入了一个回调函数
接着,回调函数返回了一个对象,也就是我们的state。如果想要使用它,就得通过create()返回的一个hook,一般叫useStore,也可以根据业务来叫,喜欢叫什么就叫什么。
最后,调用 useStore() 时,Zustand 会将订阅的状态传入,由此你可解构出你需要的部分返回。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import create from 'zustand'
const useStore = create(set => ({ counts:0 }))
const getCounts = useStore(state => state.counts)
return ( <div className="App"> <h1>{getCounts}</h1> </div> );
|
更新state:
注意到,在使用create()的时候,回调函数里还有一个set参数,它也是一个函数,可以用来创建state,当然也可以修改状态,比如下面新增了两个方法,前者让counts的值加1,后者让counts的值减1.
1 2 3 4 5 6
| const useStore = create(set => ({ count: 0, addCounts: () => set(state => ({ counts: state.counts + 1 })), decreaseCounts: () => set(state => ({ counts: state.count - 1 })), }));
|
同理,create()也能返回操作state的方法,如下:
1 2 3 4 5 6 7 8 9 10 11
| const getCounts = useStore(state => state.counts) const addCounts = useStore(state => state.addVotes); const decreaseCounts = useStore(state => state.subtractVotes);
<div className="App"> <h1>{getCounts}</h1> <button onClick={addCounts}>+</button> <button onClick={decreaseCounts}>-</button> </div>
|
而且,正如之前所说的,zustand支持局部状态更新,哪怕你的store中包含不止一个属性,你只需要关注更新的state,其他的state自动保持原样。
1 2 3 4 5 6
| const useStore = create((set) => ({ count: 0, who: '奶龙', addCounts: () => set(state => ({ votes: state.counts + 1 })), subtractCounts: () => set(state => ({ votes: state.count - 1 })), }))
|
触发addCounts后,只有counts更新了,而奶龙不变。😋
但是state就没这么方便了:
1 2 3 4 5 6 7 8
| const [store, setStore] = useState({ count: 0,Who:'奶龙' }) const addCounts = (counts) => { setStore((prevStore) => ({ ...prevStore, counts:counts + 1 })) }
|
注:zustand的局部更新只适用于第一层属性,如果是嵌套对象,那就会无效,比如下面:
1 2 3 4 5 6 7 8
| const useCountStore = create((set) => ({ nested: {Who: '奶龙', counts: 0 }, addCounts: () => set((state) => ({ nested: { counts: state.nested.counts + 1 }, })), }))
|
1 2 3 4 5 6 7 8 9 10 11 12
| function App() { const nested = useCountStore(state => state.nested) const addCounts = useCountStore(state => state.addCounts)
return ( <> <p>{JSON.stringify(nested)}</p> <button onClick={() => addCounts()}>addCounts</button>//点击之后,奶龙就消失了😭 </> ) }
|
实际上,这样才是对的:
1 2 3 4
| set((state) => ({ nested: { ...state.nested, count: state.nested.count + 1 }, }))
|
访问存储状态:
注意到,我们使用set()来创建状态。那么根据相对论,就一定有一个get()。同样的,在参数中传入get,就可以访问存储状态了。
1 2 3 4 5 6 7 8 9
| const useStore = create((set,get) => ({ counts: 0, addCounts: () => set(state => ({ votes: state.counts + 1 })), subtractCounts: () => set(state => ({ votes: state.count - 1 })), action: () => { const userCounts = get().counts } }));
|
那么问题来了,既然我可以用一个变量访问state,那我还用这个get()访问存储状态干嘛呢?所以说你只看到了第二层,而你把get想成了第一层,实际上,它在大气层:
get的优点如下:
- 避免闭包陷阱
如果在异步函数中不使用 get()
,可能会捕获旧的状态值,从而导致状态更新不正确。get()
能让你在执行时获得最新的状态,从而避免这种问题。
- 简化代码逻辑
使用 get()
不需要通过传递参数或者维护额外的中间变量,就可以即时访问当前的状态。对于某些需要依赖当前状态计算结果或条件判断的逻辑,get()
提供了一种非常直观的方式。
- 提高模块化和重用性
由于 get()
与 set()
一起被传入到 store 创建函数中,你可以把状态相关的逻辑都封装在一个函数里,这样代码更容易模块化和重用。
1 2 3 4 5 6 7 8 9
| const useStore = create((set, get) => ({ count: 0, delayedIncrement: () => { setTimeout(() => { const currentCount = get().count; set({ count: currentCount + 1 }); }, 1000); }, }));
|
get()
方法在 setTimeout
的回调函数执行时获取当前的 count
值,确保了状态更新的准确性。无论在调用 delayedIncrement
后的 1 秒内 count
的值如何变化,set()
都会基于最新的状态进行更新。
异步获取数据:
正如之前所介绍的,zustand不需要中间件,可以直接发起fetch请求来获取异步数据:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const useStore = create((set,get) => ({ counts: 0, addCounts: () => set(state => ({ votes: state.counts + 1 })), subtractCounts: () => set(state => ({ votes: state.count - 1 })), action: () => { const userCounts = get().counts } fetch: async (url) => { const res = await fetch(url) set({ counts: await res.json() }) }, }));
|
关于中间件:
1.persist:持久化状态
持久化状态是个好东西,比如在 有form的网站中, 你希望保存用户信息, 如果用户不小心刷新了页面, 就会丢失所有数据记录. 但是有了persist就不一样,persist通过 localStorage
来持久化来自应用程序的数据, 这样, 当我们刷新页面或者完全关闭页面时, 状态不会重置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { create } from 'zustand' import { persist } from 'zustand/middleware'
const useStore = create( persist( (set) => ({ count: 0, increase: () => set((state) => ({ count: state.count + 1 })), reset: () => set({ count: 0 }), }), { name: 'my-counter-store', } ) )
function Persist() { const count = useStore((state) => state.count) const increase = useStore((state) => state.increase) const reset = useStore((state) => state.reset) return ( <div> <h1>{count}</h1> <button onClick={increase}>+</button> <button onClick={reset}>Reset</button> </div> ) } export default Persist
|
2.immer:简化不可变数据更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import { create } from 'zustand' import { produce } from 'immer'
const useStore = create((set) => ({ count: 0, todos: ['学习 Zustand'], increase: () => set( produce((state) => { state.count += 1 }) ), addTodo: (todo: string) => set( produce((state) => { state.todos.push(todo) }) ), }))
function Immer() { const count = useStore((state) => state.count) const todos = useStore((state) => state.todos) const increase = useStore((state) => state.increase) const addTodo = useStore((state) => state.addTodo) return ( <div> <h2>Count: {count}</h2> <button onClick={increase}>+1</button> <h3>Todos:</h3> <ul> {todos.map((todo, idx) => ( <li key={idx}>{todo}</li> ))} </ul> <button onClick={() => addTodo('新的待办项')}>添加 Todo</button> </div> ) } export default Immer
|
这里的 produce
会自动创建一个 state
的副本,你对副本(即草稿)进行修改,最后会生成一个新的状态,而原始的状态不会被直接修改。
produce是immer最常用的函数,除了这个之外,immer还可以导入其他的函数,比如createDraft
用来创建草稿,草稿是一个原始状态的可变副本,它可以直接修改。在你修改草稿之后,可以使用 finishDraft
函数来将它转变为不可变对象;导入original
可以用来获取状态树中的原始对象。它允许你访问和比较草稿对象的原始状态,等等。
3.combine:组合多个状态片段
顾名思义,combine(initialState, createFns)
就是组合。
1 2 3 4 5 6 7 8 9 10 11 12
| import { create } from 'zustand' import { combine } from 'zustand/middleware'
const useStore = create( combine( { count: 0 }, (set) => ({ increase: () => set((state) => ({ count: state.count + 1 })), decrease: () => set((state) => ({ count: state.count - 1 })), }) ) )
|
不难看出,combine的用法非常简单,就是将值和函数合成一个完整的 store。如果是简单的全局状态管理则没必要用到combine,combine适用于实现state与 actions 分离的结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| import { create } from 'zustand' import { combine } from 'zustand/middleware'
const userSlice = combine( { user: null as null | { id: number; name: string }, }, (set) => ({ login: (name: string) => set({ user: { id: Date.now(), name } }), logout: () => set({ user: null }), }) )
const articleSlice = combine( { articles: [] as { id: number; title: string; content: string }[], }, (set) => ({ addArticle: (title: string, content: string) => set((state) => ({ articles: [ ...state.articles, { id: Date.now(), title, content }, ], })), removeArticle: (id: number) => set((state) => ({ articles: state.articles.filter((a) => a.id !== id), })), updateArticle: (id: number, newData: { title?: string; content?: string }) => set((state) => ({ articles: state.articles.map((a) => a.id === id ? { ...a, ...newData } : a ), })), }) )
const uiSlice = combine( { isLoading: false, activeTab: 'dashboard', }, (set) => ({ setLoading: (loading: boolean) => set({ isLoading: loading }), setActiveTab: (tab: string) => set({ activeTab: tab }), }) )
const useAdminStore = create((...a) => ({ ...userSlice(...a), ...articleSlice(...a), ...uiSlice(...a), }))
export default useAdminStore
|