全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-03-26_关于zustand的一些最佳实践

您的位置:首页 >> 新闻 >> 行业资讯

关于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的一些心得,欢迎大家指正。 阅读原文

上一篇:2022-06-24_测试车坠楼造成两死,蔚来回应:这是一起(非车辆原因导致的)意外事故 下一篇:2020-08-07_这个神秘的纸上游乐园,原研哉玩得很开心

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价