当前位置:首页 > 通信资讯 > 正文

react最佳实践(react 实战)

react最佳实践(react 实战)

每天都在写业务代码中度过,但是呢,经常在写业务代码的时候,会感觉自己写的某些代码有点别扭,但是又不知道是哪里别扭,今天这篇文章我整理了一些在项目中使用的一些小的技巧点。

状态逻辑复用

在使用React Hooks之前,我们一般复用的都是组件,对组件内部的状态是没办法复用的,而React Hooks的推出很好的解决了状态逻辑的复用,而在我们日常开发中能做到哪些状态逻辑的复用呢?下面我罗列了几个当前我在项目中用到的通用状态复用。

useRequest

为什么要封装这个hook呢?在数据加载的时候,有这么几点是可以提取成共用逻辑的

  • loading状态复用
  • 异常统一处理
  1. constuseRequest=()=>{
  2. const[loading,setLoading]=useState(false);
  3. const[error,setError]=useState();
  4. construn=useCallback(async(...fns)=>{
  5. setLoading(true);
  6. try{
  7. awaitPromise.all(
  8. fns.map((fn)=>{
  9. if(typeoffn==='function'){
  10. returnfn();
  11. }
  12. returnfn;
  13. })
  14. );
  15. }catch(error){
  16. setError(error);
  17. }finally{
  18. setLoading(false);
  19. }
  20. },[]);
  21. return{loading,error,run};
  22. };
  23. functionApp(){
  24. const{loading,error,run}=useRequest();
  25. useEffect(()=>{
  26. run(
  27. newPromise((resolve)=>{
  28. setTimeout(()=>{
  29. resolve();
  30. },2000);
  31. })
  32. );
  33. },[]);
  34. return(
  35. <divclassName="App">
  36. <Spinspinning={loading}>
  37. <Tablecolumns={columns}dataSource={data}></Table>
  38. </Spin>
  39. </div>
  40. );
  41. }

usePagination

我们用表格的时候,一般都会用到分页,通过将分页封装成hook,一是可以介绍前端代码量,二是统一了前后端分页的参数,也是对后端接口的一个约束。

  1. constusePagination=(
  2. initPage={
  3. total:0,
  4. current:1,
  5. pageSize:10,
  6. }
  7. )=>{
  8. const[pagination,setPagination]=useState(initPage);
  9. //用于接口查询数据时的请求参数
  10. constqueryPagination=useMemo(
  11. ()=>({limit:pagination.pageSize,offset:pagination.current-1}),
  12. [pagination.current,pagination.pageSize]
  13. );
  14. consttablePagination=useMemo(()=>{
  15. return{
  16. ...pagination,
  17. onChange:(page,pageSize)=>{
  18. setPagination({
  19. ...pagination,
  20. current:page,
  21. pageSize,
  22. });
  23. },
  24. };
  25. },[pagination]);
  26. constsetTotal=useCallback((total)=>{
  27. setPagination((prev)=>({
  28. ...prev,
  29. total,
  30. }));
  31. },[]);
  32. constsetCurrent=useCallback((current)=>{
  33. setPagination((prev)=>({
  34. ...prev,
  35. current,
  36. }));
  37. },[]);
  38. return{
  39. //用于antd表格使用
  40. pagination:tablePagination,
  41. //用于接口查询数据使用
  42. queryPagination,
  43. setTotal,
  44. setCurrent,
  45. };
  46. };

除了上面示例的两个hook,其实自定义hook可以无处不在,只要有公共的逻辑可以被复用,都可以被定义为独立的hook,然后在多个页面或组件中使用,我们在使用redux,react-router的时候,也会用到它们提供的hook。

在合适场景给useState传入函数

我们在使用useState的setState的时候,大部分时候都会给setState传入一个值,但实际上setState不但可以传入普通的数据,而且还可以传入一个函数。下面极端代码分别描述了几个传入函数的例子。

下面的代码3秒后输出什么?

如下代码所示,也有有两个按钮,一个按钮会在点击后延迟三秒然后给count + 1, 第二个按钮会在点击的时候,直接给count + 1,那么假如我先点击延迟的按钮,然后多次点击不延迟的按钮,三秒钟之后,count的值是多少?

  1. import{useState,useEffect}from'react';
  2. functionApp(){
  3. const[count,setCount]=useState(0);
  4. functionhandleClick(){
  5. setTimeout(()=>{
  6. setCount(count+1);
  7. },3000);
  8. }
  9. functionhandleClickSync(){
  10. setCount(count+1);
  11. }
  12. return(
  13. <divclassName="App">
  14. <div>count:{count}</div>
  15. <buttononClick={handleClick}>延迟加一</button>
  16. <buttononClick={handleClickSync}>加一</button>
  17. </div>
  18. );
  19. }
  20. exportdefaultApp;

我们知道,React的函数式组件会在自己内部的状态或外部传入的props发生变化时,做重新渲染的动作。实际上这个重新渲染也就是重新执行这个函数式组件。

当我们点击延迟按钮的时候,因为count的值需要三秒后才会改变,这时候并不会重新渲染。然后再点击直接加一按钮,count值由1变成了2, 需要重新渲染。这里需要注意的是,虽然组件重新渲染了,但是setTimeout是在上一次渲染中被调用的,这也意味着setTimeout里面的count值是组件第一次渲染的值。

所以即使第二个按钮加一多次,三秒之后,setTimeout回调执行的时候因为引用的count的值还是初始化的0, 所以三秒后count + 1的值就是1

如何让上面的代码延迟三秒后输出正确的值?

这时候就需要使用到setState传入函数的方式了,如下代码:

  1. import{useState,useEffect}from'react';
  2. functionApp(){
  3. const[count,setCount]=useState(0);
  4. functionhandleClick(){
  5. setTimeout(()=>{
  6. setCount((prevCount)=>prevCount+1);
  7. },3000);
  8. }
  9. functionhandleClickSync(){
  10. setCount(count+1);
  11. }
  12. return(
  13. <divclassName="App">
  14. <div>count:{count}</div>
  15. <buttononClick={handleClick}>延迟加一</button>
  16. <buttononClick={handleClickSync}>加一</button>
  17. </div>
  18. );
  19. }
  20. exportdefaultApp;

从上面代码可以看到,setCount(count + 1)被改为了setCount((prevCount) => prevCount + 1)。我们给setCount传入一个函数,setCount会调用这个函数,并且将前一个状态值作为参数传入到函数中,这时候我们就可以在setTimeout里面拿到正确的值了。

还可以在useState初始化的时候传入函数

看下面这个例子,我们有一个getColumns函数,会返回一个表格的所以列,同时有一个count状态,每一秒加一一次。

  1. functionApp(){
  2. constcolumns=getColumns();
  3. const[count,setCount]=useState(0);
  4. useEffect(()=>{
  5. setInterval(()=>{
  6. setCount((prevCount)=>prevCount+1);
  7. },1000);
  8. },[]);
  9. useEffect(()=>{
  10. console.log('columns发生了变化');
  11. },[columns]);
  12. return(
  13. <divclassName="App">
  14. <div>count:{count}</div>
  15. <Tablecolumns={columns}></Table>
  16. </div>
  17. );
  18. }

上面的代码执行之后,会发现每次count发生变化的时候,都会打印出columns发生了变化,而columns发生变化便意味着表格的属性发生变化,表格会重新渲染,这时候如果表格数据量不大,没有复杂处理逻辑还好,但如果表格有性能问题,就会导致整个页面的体验变得很差?其实这时候解决方案有很多,我们看一下如何用useState来解决呢?

  1. //将columns改为如下代码
  2. const[columns]=useState(()=>getColumns());

这时候columns的值在初始化之后就不会再发生变化了。有人提出我也可以这样写 useState(getColumns()), 实际这样写虽然也可以,但是假如getColumns函数自身存在复杂的计算,那么实际上虽然useState自身只会初始化一次,但是getColumn还是会在每次组件重新渲染的时候被执行。

上面的代码也可以简化为

  1. const[columns]=useState(getColumns);

了解hook比较算法的原理

  1. constuseColumns=(options)=>{
  2. const{isEdit,isDelete}=options;
  3. returnuseMemo(()=>{
  4. return[
  5. {
  6. title:'标题',
  7. dataIndex:'title',
  8. key:'title',
  9. },
  10. {
  11. title:'操作',
  12. dataIndex:'action',
  13. key:'action',
  14. render(){
  15. return(
  16. <>
  17. {isEdit&&<Button>编辑</Button>}
  18. {isDelete&&<Button>删除</Button>}
  19. </>
  20. );
  21. },
  22. },
  23. ];
  24. },[options]);
  25. };
  26. functionApp(){
  27. constcolumns=useColumns({isEdit:true,isDelete:false});
  28. const[count,setCount]=useState(1);
  29. useEffect(()=>{
  30. console.log('columns变了');
  31. },[columns]);
  32. return(
  33. <divclassName="App">
  34. <div>
  35. <ButtononClick={()=>setCount(count+1)}>修改count:{count}</Button>
  36. </div>
  37. <Tablecolumns={columns}dataSource={[]}></Table>
  38. </div>
  39. );
  40. }

如上面的代码,当我们点击按钮修改count的时候,我们期待只有count的值会发生变化,但是实际上columns的值也发生了变化。想了解为什么columns会发生变化,我们先了解一下react比较算法的原理。

react比较算法底层是使用的Object.is来比较传入的state的.

语法: Object.is(value1, value2);

如下代码是Object.is比较不同数据类型的数据时的返回值:

  1. Object.is('foo','foo');//trueObject.is(window,window);//trueObject.is('foo','bar');//falseObject.is([],[]);//falsevarfoo={a:1};varbar={a:1};Object.is(foo,foo);//trueObject.is(foo,bar);//falseObject.is(null,null);//true//特例Object.is(0,-0);//falseObject.is(0,+0);//trueObject.is(-0,-0);//trueObject.is(NaN,0/0);//true

通过上面的代码可以看到,Object.is对于对象的比较是比较引用地址的,而不是比较值的,所以Object.is([], []), Object.is({},{})的结果都是false。而对于基础类型来说,大家需要注意的是最末尾的四个特列,这是与===所不同的。

再回到上面代码的例子中,useColumns将传入的options作为useMemo的第二个参数,而options是一个对象。当组件的count状态发生变化的时候,会重新执行整个函数组件,这时候useColumns会被调用然后传入{ isEdit: true, isDelete: false },这是一个新创建的对象,与上一次渲染所创建的options的内容虽然一致,但是Object.is比较结果依然是false,所以columns的结果会被重新创建返回。

通过二次封装标准化组件

我们在项目中使用antd作为组件库,虽然antd可以满足大部分的开发需要,但是有些地方通过对antd进行二次封装,不仅可以减少开发代码量,而且对于页面的交互起到了标准化作用。

看一下下面这个场景, 在我们开发一个数据表格的时候,一般会用到哪些功能呢?

  1. 表格可以分页
  2. 表格最后一列会有操作按钮
  3. 表格顶部会有搜索区域
  4. 表格顶部可能会有操作按钮

还有其他等等一系列的功能,这些功能在系统中会大量使用,而且其实现方式基本是一致的,这时候如果能把这些功能集成到一起封装成一个标准的组件,那么既能减少代码量,而且也会让页面展现上更加统一。

以封装表格操作列为例,一般用操作列我们会像下面这样封装

  1. constcolumns=[{title:'操作',dataIndex:'action',key:'action',width:'10%',align:'center',render:(_,row)=>{return(<><Buttontype="link"onClick={()=>handleEdit(row)}>编辑</Button><Popconfirmtitle="确认要删除?"onConfirm={()=>handleDelete(row)}><Buttontype="link">删除</Button></Popconfirm></>);}}]

我们期望的是操作列也可以像表格的columns一样通过配置来生成,而不是写jsx。看一下如何封装呢?

  1. //定义操作按钮exportinterfaceIActionextendsOmit<ButtonProps,'onClick'>{//自定义按钮渲染render?:(row:any,index:number)=>React.ReactNode;onClick?:(row:any,index:number)=>void;//是否有确认提示confirm?:boolean;//提示文字confirmText?:boolean;//按钮显示文字text:string;}//定义表格列exportinterfaceIColumn<T=any>extendsColumnType<T>{actions?:IAction[];}//然后我们可以定义一个hooks,专门用来修改表格的columns,添加操作列constuseActionButtons=(columns:IColumn[],actions:IAction[]|undefined):IColumn[]=>{returnuseMemo(()=>{if(!actions||actions.length===0){returncolumns;}return[...columns,{align:'center',title:'操作',key:'__action',dataIndex:'__action',width:Math.max(120,actions.length*85),render(value:any,row:any,index:number){returnactions.map((item)=>{if(item.render){returnitem.render(row,index);}if(item.confirm){return<Popconfirmtitle={item.confirmText||'确认要删除?'}onConfirm={()=>item.onClick?.(row,index)}><Buttontype="link">{item.text}</Button></Popconfirm>}return(<Button{...item}type="link"key={item.text}onClick={()=>item.onClick?.(row,index)}>{item.text}</Button>);});}}];},[columns,actions,actionFixed]);};//最后我们对表格再做一个封装constCustomTable:React.FC<ITableProps>=({actions,columns,...props})=>{constactionColumns=useActionColumns(columns,actions)//渲染表格}

通过上面的封装,我们再使用表格的时候,就可以这样去写

  1. constactions:IAction[]=[{text:'编辑',onClick:handleModifyRecord,},];return<CustomTableactions={actions}columns={columns}></CustomTable>

前端进击者

原文链接:https://mp.weixin.qq.com/s/qJl73MkIYz72PDKx2nEjAA

如果您对该产品感兴趣,请填写办理(客服微信:xiaoxiongyidong)

为您推荐:

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。