React实践指南

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

状态逻辑复用

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

useRequest

为何要封装这个hook呢?在数据加载的时候,有这么几点是能够提取成共用逻辑的算法

  1. loading状态复用
  2. 异常统一处理
const useRequest = () => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState();

  const run = useCallback(async (...fns) => {
    setLoading(true);
    try {
      await Promise.all(
        fns.map((fn) => {
          if (typeof fn === 'function') {
            return fn();
          }
          return fn;
        })
      );
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
  }, []);

  return { loading, error, run };
};

function App() {
  const { loading, error, run } = useRequest();
  useEffect(() => {
    run(
      new Promise((resolve) => {
        setTimeout(() => {
          resolve();
        }, 2000);
      })
    );
  }, []);
  return (
    <div className="App"> <Spin spinning={loading}> <Table columns={columns} dataSource={data}></Table> </Spin> </div>
  );
}
复制代码

usePagination

咱们用表格的时候,通常都会用到分页,经过将分页封装成hook,一是能够介绍前端代码量,二是统一了先后端分页的参数,也是对后端接口的一个约束。redux

const usePagination = ( initPage = { total: 0, current: 1, pageSize: 10, } ) => {
  const [pagination, setPagination] = useState(initPage);

  // 用于接口查询数据时的请求参数
  const queryPagination = useMemo(
    () => ({ limit: pagination.pageSize, offset: pagination.current - 1 }),
    [pagination.current, pagination.pageSize]
  );

  const tablePagination = useMemo(() => {
    return {
      ...pagination,
      onChange: (page, pageSize) => {
        setPagination({
          ...pagination,
          current: page,
          pageSize,
        });
      },
    };
  }, [pagination]);

  const setTotal = useCallback((total) => {
    setPagination((prev) => ({
      ...prev,
      total,
    }));
  }, []);
  const setCurrent = useCallback((current) => {
    setPagination((prev) => ({
      ...prev,
      current,
    }));
  }, []);

  return {
    // 用于antd 表格使用
    pagination: tablePagination,
    // 用于接口查询数据使用
    queryPagination,
    setTotal,
    setCurrent,
  };
};
复制代码

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

在合适场景给useState传入函数

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

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

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

import { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setTimeout(() => {
      setCount(count + 1);
    }, 3000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div className="App"> <div>count:{count}</div> <button onClick={handleClick}>延迟加一</button> <button onClick={handleClickSync}>加一</button> </div>
  );
}

export default App;
复制代码

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

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

因此即便第二个按钮加一屡次,三秒以后,setTimeout回调执行的时候由于引用的count的值仍是初始化的0, 因此三秒后count + 1的值就是1app

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

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

import { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setTimeout(() => {
      setCount((prevCount) => prevCount + 1);
    }, 3000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div className="App"> <div>count:{count}</div> <button onClick={handleClick}>延迟加一</button> <button onClick={handleClickSync}>加一</button> </div>
  );
}

export default App;
复制代码

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

还能够在useState初始化的时候传入函数

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

function App() {
  const columns = getColumns();
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setCount((prevCount) => prevCount + 1);
    }, 1000);
  }, []);

  useEffect(() => {
    console.log('columns发生了变化');
  }, [columns]);
  return (
    <div className="App"> <div>count: {count}</div> <Table columns={columns}></Table> </div>
  );
}
复制代码

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

// 将columns改成以下代码
const [columns] = useState(() => getColumns());
复制代码

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

上面的代码也能够简化为

const [columns] = useState(getColumns);
复制代码

了解hook比较算法的原理

const useColumns = (options) => {
  const { isEdit, isDelete } = options;
  return useMemo(() => {
    return [
      {
        title: '标题',
        dataIndex: 'title',
        key: 'title',
      },
      {
        title: '操做',
        dataIndex: 'action',
        key: 'action',
        render() {
          return (
            <> {isEdit && <Button>编辑</Button>} {isDelete && <Button>删除</Button>} </>
          );
        },
      },
    ];
  }, [options]);
};

function App() {
  const columns = useColumns({ isEdit: true, isDelete: false });
  const [count, setCount] = useState(1);

  useEffect(() => {
    console.log('columns变了');
  }, [columns]);
  return (
    <div className="App"> <div> <Button onClick={() => setCount(count + 1)}>修改count:{count}</Button> </div> <Table columns={columns} dataSource={[]}></Table> </div>
  );
}
复制代码

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

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

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

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

Object.is('foo', 'foo');     // true
Object.is(window, window);   // true

Object.is('foo', 'bar');     // false
Object.is([], []);           // false

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);         // true
Object.is(foo, bar);         // false

Object.is(null, null);       // true

// 特例
Object.is(0, -0);            // false
Object.is(0, +0);            // true
Object.is(-0, -0);           // true
Object.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. 表格顶部可能会有操做按钮

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

以封装表格操做列为例,通常用操做列咱们会像下面这样封装

const columns = [{
        title: '操做',
        dataIndex: 'action',
        key: 'action',
        width: '10%',
        align: 'center',
        render: (_, row) => {
          return (
            <> <Button type="link" onClick={() => handleEdit(row)}> 编辑 </Button> <Popconfirm title="确认要删除?" onConfirm={() => handleDelete(row)}> <Button type="link">删除</Button> </Popconfirm> </>
          );
        }
      }]
复制代码

咱们指望的是操做列也能够像表格的columns同样经过配置来生成,而不是写jsx。看一下如何封装呢?

// 定义操做按钮
export interface IAction extends Omit<ButtonProps, 'onClick'> {
  // 自定义按钮渲染
  render?: (row: any, index: number) => React.ReactNode;
  onClick?: (row: any, index: number) => void;
  // 是否有确认提示
  confirm?: boolean;
  // 提示文字
  confirmText?: boolean;
  // 按钮显示文字
  text: string;
}
// 定义表格列
export interface IColumn<T = any> extends ColumnType<T> {
  actions?: IAction[];
}

// 而后咱们能够定义一个hooks,专门用来修改表格的columns,添加操做列
const useActionButtons = (
  columns: IColumn[],
  actions: IAction[] | undefined
): IColumn[] => {
  return useMemo(() => {
    if (!actions || actions.length === 0) {
      return columns;
    }
    return [
      ...columns,
      {
        align: 'center',
        title: '操做',
        key: '__action',
        dataIndex: '__action',
        width: Math.max(120, actions.length * 85),
        render(value: any, row: any, index: number) {
          return actions.map((item) => {
            if (item.render) {
              return item.render(row, index);
            }
            if(item.confirm) {
              return <Popconfirm title={item.confirmText || '确认要删除?'} onConfirm={() => item.onClick?.(row, index)}> <Button type="link">{item.text}</Button> </Popconfirm>
            }
            return (
              <Button {...item} type="link" key={item.text} onClick={() => item.onClick?.(row, index)} > {item.text} </Button>
            );
          });
        }
      }
    ];
  }, [columns, actions, actionFixed]);
};

// 最后咱们对表格再作一个封装
const CustomTable: React.FC<ITableProps> = ({ actions, columns, ...props }) => {
  const actionColumns = useActionColumns(columns,actions)
  // 渲染表格
}
复制代码

经过上面的封装,咱们再使用表格的时候,就能够这样去写

const actions: IAction[] = [
    {
      text: '编辑',
      onClick: handleModifyRecord,
    },
  ];

return <CustomTable actions={actions} columns={columns}></CustomTable>
复制代码

避免重复渲染

重复渲染,包含重复计算,重复发请求等等,这个在开发中很容易遇到。好比某一个页面代码的时候,某个接口被调用了两次,对于这种状况,咱们仍是须要去尽可能避免的。

先看一下下面几个示例代码

示例一
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(1);
    setTimeout(() => {
      setCount(0);
    }, 1000);
  }, [count]);

  return <div>{count}</div>;
}
复制代码
示例二
//组件
import React, { useEffect } from 'react';

const Test = () => {
  useEffect(() => {
    console.log('此处发送请求');
  }, []);

  return <div></div>;
};
export default Test;

// 页面
function App() {
   const [count, setCount] = useState(0);
   useEffect(() => {
     setTimeout(() => {
       setCount(1)
     },0)
   },[])
  return <> <Route exact key="test" path="/" component={() => <Test></Test>} /> </>
}
复制代码
示例三
function App() {
  const [pageSize, setPageSize] = useState(10);
  const [currentPage, setCurrentPage] = useState(1);
  const [update, setUpdate] = useState(0);
  const [appCode, setAppCode] = useState('');
  useEffect(() => {
    console.log('发送请求');
  }, [pageSize, currentPage, update]);

  // 当 appCode 值发生变化时,修改 update 从而从新请求数据
  useEffect(() => {
    setUpdate(update + 1);
  }, [appCode]);

  return <div></div>;
}
复制代码

请问,上面三个示例存在什么问题呢?

第一个:会致使死循环

第二个:进入页面会发送两次请求

第三个:进入页面会发送两次请求

接下来咱们来逐一分析缘由

分析示例一
useEffect(() => {
    setCount(1);
    setTimeout(() => {
      setCount(0);
    }, 1000);
  }, [count]);
复制代码

上面代码为示例一中的useEffect,能够看到useEffect监听的是count的变化,并且里面有一个setTimeout会每一秒钟修改一次count的值,而count的变化又会致使useEffect从新被执行,而后就进入了死循环。那么应该如何解决呢?方法就是useEffect不要去监听count的变化。即改成

useEffect(() => {
    setCount(1);
    setTimeout(() => {
      setCount(0);
    }, 1000);
  }, []);
复制代码
分析示例二

示例二关键问题在于下面这段代码

<Route exact key="test" path="/" component={() => <Test></Test>} />
复制代码

在代码中,count的值初始化为1,而后一秒钟后被修改成0, 这会致使App组件产生两次渲染,注意上面的代码component传入的参数是一个箭头函数,而两次渲染会致使初始化两个箭头函数,这就致使两次给Route传入的component是不同的,从而产生两次渲染,Test组件也就被渲染了两次,从而内部 的请求发送了两次。如何去修改呢?

<Route exact key="test" path="/" component={Test} />
复制代码
分析示例三

示例三中为了在appCode发生变化时从新请求数据,而后加了一个update属性,经过调整这个属性来触发useEffect执行,可是问题就在于下面这段代码

// 当 appCode 值发生变化时,修改 update 从而从新请求数据
  useEffect(() => {
    setUpdate(update + 1);
  }, [appCode]);
复制代码

初始化页面的时候,useEffect会被默认执行一遍,因此初始化的时候会发送一个请求,同时上面的useEffect也会被执行,这时候update发生变化了,因此又会致使请求再发送一次,如何调整呢?

其实彻底不须要update,直接在下面代码监听appCode就行了

useEffect(() => {
    console.log('发送请求');
  }, [pageSize, currentPage, appCode]);
复制代码
相关文章
相关标签/搜索