拥抱 Zustand 的优雅——你的 React 状态管理就该这么丝滑!
前言
React的状态管理库有很多种,大致可以分为以下几种:
- React内置状态管理库,useState/useReducer、useContext
- 全局状态管理库,Redux、MobX、Zustand
- 原子化状态管理库,Recoil、Jotai
作为后起之秀的Zustand,以轻量简洁、高性能、支持TS深受广大开发者的喜爱。
周下载量更是高达六百多万。
Zustand的使用
安装
npm install zustand
快速上手
声明状态
import { create } from "zustand";
type Action = {
toggleTheme: () => void;
};
type State = { theme: string}
const themeStore = create<Action & State>((set) => ({
theme: "light",
toggleTheme: () => {
return set((state) => ({
theme: state.theme === "light" ? "dark" : "light",
}))
},
}));
export default themeStore;
使用
import { useEffect } from 'react'
import themeStore from './store/theme'
function ThemeChange() {
const theme = useThemeStore((state) => state.theme)
const toggleTheme = useThemeStore((state) => state.toggleTheme)
return <button onClick={toggleTheme}>toggle theme: {theme}</button>
}
就这样我们简单创建了一个主题切换的转态管理库,是不是 so easy。
以上我们只是创建了一个单纯的主题状态切换,切换主题后,我们应该如何监听主题的变化去修改样式嘞?那就需要用到subscribe了。
useEffect(() => {
const unsub = useThemeStore.subscribe((state) => {
document.body.classList.remove('light', 'dark');
document.body.classList.add(state.theme);
});
// 初始化
document.body.classList.remove('light', 'dark');
document.body.classList.add(useThemeStore.getState().theme);
return () => unsub();
}, []);
Zustand中间件
Persist数据持久化
有一些数据可能需要存在浏览器内存中,以便你在页面重新加载或应用程序重新启动时持久化存储的状态,比如说上个例子中的主题状态就需要持久化,以便用户能拥有更好的体验效果。
import { create } from "zustand";
import { persist } from "zustand/middleware";
type State = { theme: string };
type Action = { toggleTheme: () => void };
const useThemeStore = create<State & Action>()(
persist(
(set) => ({
theme: "light",
toggleTheme: () => {
set((state) => ({
theme: state.theme === "light" ? "dark" : "light",
}));
},
}),
{ name: "themeStore" }
)
);
export default useThemeStore;
Devtools调试
全局状态一旦多了我们就不好把控,需要借助调试工具去调试状态的变化,借助Redux DevTools我们可以去调试Zustand的状态变化。
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
type State = { theme: string };
type Action = { toggleTheme: () => void };
const useThemeStore = create<State & Action>()(
devtools(
persist(
(set) => ({
theme: "light",
toggleTheme: () => {
set((state) => ({
theme: state.theme === "light" ? "dark" : "light",
}));
},
}),
{ name: "themeStore" }
)
)
);
export default useThemeStore;
Immer执行不可变
在React中对于一些对象的更改,我们可以ES6的解构语法{...XXX},将其解构出来出来后重新赋值,而immer中间件让您执行不可变的更新,操作更加方便。
在开始的时候我们需要安装immer库
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
type State = {
person: {
name: string;
age: number;
email: string;
address: {
street: string;
city: string;
state: string;
};
};
};
type Action = {
setName: (name: string) => void;
setCity: (city: string) => void;
};
// ES6解构的方法
// const usePersonStore = create<State & Action>()(
// devtools((set) => ({
// person: {
// name: "John",
// age: 30,
// email: "<EMAIL>",
// address: {
// street: "123 Main St",
// city: "New York",
// state: "NY",
// },
// },
// setName: (name) =>
// set((state) => ({ ...state, person: { ...state.person, name } })),
// setCity: (city) =>
// set((state) => ({
// ...state,
// person: { ...state.person, address: { ...state.person.address, city } },
// })),
// }))
// );
const usePersonStore = create<State & Action>()(
devtools(
immer((set) => ({
person: {
name: "John",
age: 30,
email: "<EMAIL>",
address: {
street: "123 Main St",
city: "New York",
state: "NY",
},
},
setName: (name) =>
set((state) => {
state.person.name = name;
}),
setCity: (city) =>
set((state) => {
state.person.address.city = city;
}),
}))
)
);
export default usePersonStore;
Combine自动推断类型
以往我们需要像上面那样显示定义类型,现在我们可以通过combine去隐式定义,自动帮我们推断出类型,又加快了开发速度。不得不说,zustand项目的ts运用得真6。
import { create } from "zustand";
import { combine, devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
const usePersonStore = create(
devtools(
immer(
combine(
{
person: {
name: "John",
age: 30,
email: "<EMAIL>",
address: {
street: "123 Main St",
city: "New York",
state: "NY",
},
},
},
(set) => ({
setName: (name: string) =>
set((state) => {
state.person.name = name;
}),
setCity: (city: string) =>
set((state) => {
state.person.address.city = city;
}),
})
)
)
)
);
export default usePersonStore;
SubscribeWithSelector精准订阅
Store存在多个状态,在订阅的时候只要一触发事件,都是重新订阅,降低了性能。
import { create } from "zustand";
import { combine, devtools } from "zustand/middleware";
const useThemeStore = create(
devtools(
combine(
{
theme: "dark",
lang: "zh"
},
(set) => ({
toggleTheme: () => {
set((state) => ({
theme: state.theme === "light" ? "dark" : "light",
}));
},
toggleLang: () => {
set((state) => ({
lang: state.lang === "zh" ? "en" : "zh",
}));
},
})
)
)
);
export default useThemeStore;
useEffect(() => {
// 监听 theme 变化并切换 body class
const unsub = useThemeStore.subscribe((state) => {
console.log('触发事件', state.theme, state.lang);
document.body.classList.remove('light', 'dark');
document.body.classList.add(state.theme);
});
// 初始化
document.body.classList.remove('light', 'dark');
document.body.classList.add(useThemeStore.getState().theme);
return () => unsub();
}, []);
在上面这个代码中两个方法,toggleTheme、toggleLang的触发都会订阅,打印触发事件,而这是只需要监听主题的变化,我们可以用subscribeWithSelector进行改造。
import { create } from "zustand";
import { combine, devtools } from "zustand/middleware";
import { subscribeWithSelector } from "zustand/middleware";
const useThemeStore = create(
subscribeWithSelector(
devtools(
combine(
{
theme: "dark",
lang: "zh"
},
(set) => ({
toggleTheme: () =>
set((state) => ({
theme: state.theme === "dark" ? "light" : "dark",
})),
toggleLang: () =>
set((state) => ({
lang: state.lang === "zh" ? "en" : "zh",
})),
})
)
)
)
);
export default useThemeStore;
useEffect(() => {
// 监听 theme 变化并切换 body class
const unsub = useThemeStore.subscribe((state) => state.theme, (theme) => {
console.log('触发事件', theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
});
// 初始化
document.body.classList.remove('light', 'dark');
document.body.classList.add(useThemeStore.getState().theme);
return () => unsub();
}, []);
在触发toggleLang事件就不会打印触发事件。
Redux模仿reducer操作
习惯了redux的操作习惯,切换zustand的操作还不习惯,我们可以使用redux中间件,使得继续像reducer一样操作状态的切换。
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
// 定义 state 和 action 类型
type CounterState = {
count: number
}
type CounterActions = {
dispatch: (action: { type: string; payload?: any }) => void
}
// reducer 函数
function counterReducer(state: CounterState, action: { type: string; payload?: any }): CounterState {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }
case 'DECREMENT':
return { count: state.count - 1 }
case 'ADD':
return { count: state.count + (action.payload ?? 0) }
default:
return state
}
}
// 创建 store,暴露 dispatch
const useCounterReduxLike = create<CounterState & CounterActions>()(
devtools((set, get) => ({
count: 0,
dispatch: (action) => set((state) => counterReducer(state, action)),
}))
)
export default useCounterReduxLike;
import useCounterReduxLike from './store/counterReduxLike';
function Counter() {
const count = useCounterReduxLike((s) => s.count)
const dispatch = useCounterReduxLike((s) => s.dispatch)
return (
<div>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<span>{count}</span>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'ADD', payload: 5 })}>+5</button>
</div>
)
}
总结
Zustand作为新生的react全局状态管理库,简单明了,容易上手,其中中间件更是赋予了新的活力。
我们学了Persist 持久化、Devtools 调试、Immer 不可变...... 学完这篇,现在你的 React 状态管理直接起飞 🚀。