关于zustand的一些最佳实践
前言看过我文章的人,应该知道React状态管理库中我比较喜欢使用Zustand的,因为使用起来非常简单,没有啥心智负担。这篇文章给大家分享一下,我这段时间使用zustand的一些心得和个人认为的最佳实践。
优化在React项目里,最重要优化可能就是解决重复渲染的问题了。使用zustand的时候,如果不小心,也会导致一些没用的渲染。
举个例子:
创建一个存放主题和语言类型的store
import{create}from'zustand';
interfaceState{
theme:string;
lang:string;
}
interfaceAction{
setTheme:(theme:string)=void;
setLang:(lang:string)=void;
}
constuseConfigStore=createStateAction((set)=({
theme:'light',
lang:'zh-CN',
setLang:(lang:string)=set({lang}),
setTheme:(theme:string)=set({theme}),
}));
exportdefaultuseConfigStore;
分别创建两个组件,主题组件和语言类型组件
import useConfigStore from './store';
const Theme = () = {
const { theme, setTheme } = useConfigStore();
console.log('theme render');
return (
div
div{theme}/div
button onClick={() = setTheme(theme === 'light' ? 'dark' : 'light')}/button
/div
)
}
export default Theme;
import useConfigStore from './store';
const Lang = () = {
const { lang, setLang } = useConfigStore();
console.log('lang render...');
return (
div
div{lang}/div
button onClick={() = setLang(lang === 'zh-CN' ? 'en-US' : 'zh-CN')}/button
/div
)
}
export default Lang;
按照上面写法,改变theme会导致Lang组件渲染,改变lang会导致Theme重新渲染,但是实际上这两个都没有关系,怎么优化这个呢,有以下几种方法。
方案一 import useConfigStore from './store';
const Theme = () = {
const theme = useConfigStore((state) = state.theme);
const setTheme = useConfigStore((state) = state.setTheme);
console.log('theme render');
return (
div
div{theme}/div
button onClick={() = setTheme(theme === 'light' ? 'dark' : 'light')}/button
/div
)
}
export default Theme;
把值单个return出来,zustand内部会判断两次返回的值是否一样,如果一样就不重新渲染。
这里因为只改变了lang,theme和setTheme都没变,所以不会重新渲染。
方案二 上面写法如果变量很多的情况下,要写很多遍useConfigStore,有点麻烦。可以把上面方案改写成这样,变量多的时候简单一些。
import useConfigStore from './store';
const Theme = () = {
const { theme, setTheme } = useConfigStore(state = ({
theme: state.theme,
setTheme: state.setTheme,
}));
console.log('theme render');
return (
div
div{theme}/div
button onClick={() = setTheme(theme === 'light' ? 'dark' : 'light')}/button
/div
)
}
export default Theme;
上面这种写法是不行的,因为每次都返回了新的对象,即使theme和setTheme不变的情况下,也会返回新对象,zustand内部拿到返回值和上次比较,发现每次都是新的对象,然后重新渲染。
上面情况,zustand提供了解决方案,对外暴露了一个useShallow方法,可以浅比较两个对象是否一样。
import { useShallow } from 'zustand/react/shallow';
import useConfigStore from './store';
const Theme = () = {
const { theme, setTheme } = useConfigStore(
useShallow(state = ({
theme: state.theme,
setTheme: state.setTheme,
}))
);
console.log('theme render');
return (
div
div{theme}/div
button onClick={() = setTheme(theme === 'light' ? 'dark' : 'light')}/button
/div
)
}
export default Theme;
方案三 上面两种写法是官方推荐的写法,但是我觉得还是很麻烦,我自己封装了一个useSelector方法,使用起来更简单一点。
import{pick}from'lodash-es';
import{useRef}from'react';
import{shallow}from'zustand/shallow';
typePickT,KextendskeyofT={
[PinK]:T[P];
};
typeMany=T|readonly
exportfunctionuseSelectorSextendsobject,PextendskeyofS(
paths:Many
):(state:S)=PickS,P{
constprev=useRefPickS,P({}asPickS,P
return(state:S)={
if(state){
constnext=pick(state,paths);
returnshallow(prev.current,next)?prev.current:(prev.current=next);
}
returnprev.current;
}
useSelector主要使用了lodash里的pick方法,然后使用了zustand对外暴露的shallow方法,进行对象浅比较。
import useConfigStore from './store';
import { useSelector } from './use-selector';
const Theme = () = {
const { theme, setTheme } = useConfigStore(
useSelector(['theme', 'setTheme'])
);
console.log('theme render');
return (
div
div{theme}/div
button onClick={() = setTheme(theme === 'light' ? 'dark' : 'light')}/button
/div
)
}
export default Theme;
封装的useSelector只需要传入对外暴露的字符串数组就行了,不用再写方法了,省了很多代码,同时还保留了ts的类型推断。
image.pngimage.png终极方案 image.png看一下这个代码,分析一下,前面theme和setTheme和后面useSelector的参数是一样的,那我们能不能写一个插件,自动把const { theme, setTheme } = useStore();转换为const { theme, setTheme } = useStore(useSelector(['theme', 'setTheme']));,肯定是可以的。
因为项目是vite项目,所以这里写的是vite插件,webpack插件实现和这个差不多。
因为要用到babel代码转换,所以需要安装babel几个依赖
pnpmi@babel/generator@babel/parser@babel/traverse@babel/types-D
@babel/parser可以把代码转换为抽象语法树
@babel/traverse可以转换代码
@babel/generator把抽象语法树生成代码
@babel/types快速创建节点
插件完整代码,具体可以看一下代码注释
importgeneratefrom'@babel/generator';
importparsefrom'@babel/parser';
importtraversefrom"@babel/traverse";
import*astfrom'@babel/types';
exportdefaultfunctionzustand(){
return{
name:'zustand',
transform(src,id){
//过滤非.tsx文件
if(!/\.tsx?$/.test(id)){
return{
code:src,
map:null,//如果可行将提供sourcemap
}
//把代码转换为ast
constast=parse.parse(src,{sourceType:'module'
letflag=false;
traverse.default(ast,{
VariableDeclarator:function(path){
//找到变量为useStore
if(path.node?.init?.callee?.name==='useStore'){
//获取变量名
constkeys=path.node.id.properties.map(o=o.value.name);
//给useStore方法注入useSelector参数
path.node.init.arguments=[
t.callExpression(
t.identifier('useSelector'),
[t.arrayExpression(
keys.map(o=t.stringLiteral(o)
))]
)
flag=true;
}
},
if(flag){
//如果没有找到useSelector,则自动导入useSelector方法
if(!src.includes('useSelector')){
ast.program.body.unshift(
t.importDeclaration([
t.importSpecifier(
t.identifier('useSelector'),
t.identifier('useSelector')
)],
t.stringLiteral('useSelector')
)
)
}
//通过ast生成代码
const{code}=generate.default(ast);
return{
code,
map:null,
}
}
return{
code:src,
map:null,
},
}
在vite配置中,引入刚才写的插件
image.png把Theme里useSelector删除
image.png看一下转换后的文件,把useSelector自动注入进去了
image.png持久化把zustand里的数据持久化到localstorage或sessionStorage中,官方提供了中间件,用起来很简单,我想和大家分享的是,只持久化某个字段,而不是整个对象。
持久化整个对象
import{create}from'zustand';
import{createJSONStorage,persist}from'zustand/middleware';
interfaceState{
theme:string;
lang:string;
}
interfaceAction{
setTheme:(theme:string)=void;
setLang:(lang:string)=void;
}
constuseConfigStore=create(
persistStateAction(
(set)=({
theme:'light',
lang:'zh-CN',
setLang:(lang:string)=set({lang}),
setTheme:(theme:string)=set({theme}),
}),
{
name:'config',
storage:createJSONStorage(()=localStorage),
}
)
);
exportdefaultuseConfigStore;
image.png如果想只持久化某个字段,可以使用partialize方法
image.pngimage.png调试当store里数据变得复杂的时候,可以使用redux-dev-tools浏览器插件来查看store里的数据,不过需要使用devtools中间件。
image.png可以看到每一次值的变化
image.png默认操作名称都是anonymous这个名字,如果我们想知道调用了哪个函数,可以给set方法传第三个参数,这个表示方法名。
image.pngimage.png还可以回放动作
image.png多实例zustand的数据默认是全局的,也就是说每个组件访问的数据都是同一个,那如果写了一个组件,这个组件在多个地方使用,如果用默认方式,后面的数据会覆盖掉前面的,这个不是我们想要的。
为了解决这个问题,官方推荐这样做:
importReact,{createContext,useRef}from'react';
import{StoreApi,createStore}from'zustand';
interfaceState{
theme:string;
lang:string;
}
interfaceAction{
setTheme:(theme:string)=void;
setLang:(lang:string)=void/**/;
}
exportconstStoreContext=createContextStoreApiStateAction(
{}asStoreApiStateAction
);
exportconstStoreProvider=({children}:any)={
conststoreRef=useRefStoreApiStateAction();
if(!storeRef.current){
storeRef.current=createStoreStateAction((set)=({
theme:'light',
lang:'zh-CN',
setLang:(lang:string)=set({lang}),
setTheme:(theme:string)=set({theme}),
}));
}
returnReact.createElement(
StoreContext.Provider,
{value:storeRef.current},
children
);
};
使用了React的context
使用Theme组件来模拟两个实例,使用StoreProvider包裹Theme组件
import './App.css'
import { StoreProvider } from './store'
import Theme from './theme'
function App() {
return (
StoreProvider
Theme /
/StoreProvider
StoreProvider
Theme /
/StoreProvider
)
}
export default App
Theme组件
import { useContext } from 'react';
import { useStore } from 'zustand';
import { StoreContext } from './store';
const Theme = () = {
const store = useContext(StoreContext);
const { theme, setTheme } = useStore(store);
return (
div
div{theme}/div
button onClick={() = setTheme(theme === 'light' ? 'dark' : 'light')}/button
/div
)
}
export default Theme;
image.png可以看到两个实例没有公用数据了
官网推荐的方法,虽然可以实现多实例,但是感觉有点麻烦,我自己给封装了一下,把Context、Provider、useStore使用工厂方法统一导出,使用起来更加简单。
importReact,{useContext,useRef}from'react';
import{
StateCreator,
StoreApi,
createStore,
useStoreasuseExternalStore,
}from'zustand';
typeExtractState=Sextends{getState:()=inferX}?X:never;
exportconstcreateContext=T(store:StateCreatorT,[],[])={
constContext=React.createContextStoreApi({}asStoreApi);
constProvider=({children}:any)={
conststoreRef=useRefStoreApi|undefined
if(!storeRef.current){
storeRef.current=createStore(store);
}
returnReact.createElement(
Context.Provider,
{value:storeRef.current},
children
functionuseStore():T;
functionuseStoreU(selector:(state:ExtractStateStoreApi)=U):U;
functionuseStoreU(selector?:(state:ExtractStateStoreApi)=U):U{
conststore=useContext(Context);
//eslint-disable-next-line@typescript-eslint/ban-ts-comment
//@ts-ignore
returnuseExternalStore(store,selector);
}
return{Provider,Context,useStore};
};
引入Provider
import './App.css'
import Theme from './theme'
import { Provider } from './store'
function App() {
return (
Provider
Theme /
/Provider
Provider
Theme /
/Provider
)
}
export default App
在Theme组件中使用useStore,并且可以和前面封装的useSelector配合使用。
import { useStore } from './store';
import { useSelector } from './use-selector';
const Theme = () = {
const { theme, setTheme } = useStore(useSelector(['theme', 'setTheme']));
return (
div
div{theme}/div
button onClick={() = setTheme(theme === 'light' ? 'dark' : 'light')}/button
/div
)
}
export default Theme;
最后以上就是我这段时间使用zustand的一些心得,欢迎大家指正。
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线