React最佳实践

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

状态逻辑复用

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

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,一是可以介绍前端代码量,二是统一了前后端分页的参数,也是对后端接口的一个约束。

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的值是多少?

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发生变化时,做重新渲染的动作。实际上这个重新渲染也就是重新执行这个函数式组件。

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

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

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

这时候就需要使用到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');     // trueObject.is(window, window);   // trueObject.is('foo', 'bar');     // falseObject.is([], []);           // falsevar foo = { a: 1 };var bar = { 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. 表格顶部可能会有操作按钮

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

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

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>

本文分享自微信公众号 - 前端有的玩(gh_918bae0a9616),作者:前端进击者

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-07-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【转】Mobx React  最佳实践

    在这一篇文章里,将展示一些使用了mobx的React的最佳实践方式,并按照一条一条的规则来展示。在你遇到问题的时候,可以依照着这些规则来解决。 这篇文章要求你对...

    Tiffany_c4df
  • 你不知道的 React 最佳实践

    React 是一个用于开发用户界面的 JavaScript 库, 是由 Facebook 在 2013 年创建的。 React 集成了许多令人兴奋的组件、库和框...

    一只图雀
  • React + Redux 最佳实践

    时见疏星
  • 6个React Hook最佳实践技巧

    在过去,像状态和生命周期函数这样的 React 特性只适用于基于类的组件。基于函数的组件被称为哑(dumb)、瘦(skinny)或表示(presentation...

    深度学习与Python
  • 用TypeScript编写React的最佳实践

    如今, React 和 TypeScript 是许多开发人员正在使用的两种很棒的技术。但是把他们结合起来使用就变得很棘手了,有时很难找到正确的答案。不要担心,本...

    ConardLi
  • 我们编写 React 组件的最佳实践

    刚接触 的时候,在一个又一个的教程上面看到很多种编写组件的方法,尽管那时候 框架已经相当成熟,但是并没有一个固定的规则去规范我们去写代码。 在过去的一年里,...

    企鹅号小编
  • 使用 React&Mobx 的几个最佳实践

    Mobx 是我非常喜欢的 React 状态管理库,它非常灵活,同时它的灵活也会给开发带来非常多的问题,因此我们在开发的时候也要遵循一些写法上的最佳实践,使我们的...

    ConardLi
  • React 代码共享最佳实践方式

    任何一个项目发展到一定复杂性的时候,必然会面临逻辑复用的问题。在React中实现逻辑复用通常有以下几种方式:Mixin、高阶组件(HOC)、修饰器(decora...

    winty
  • React 条件渲染最佳实践(7 种方法)

    在 React 中,条件渲染可以通过多种方式,不同的使用方式场景取决于不同的上下文。在本文中,我们将讨论所有可用于为 React 中的条件渲染编写更好的代码的方...

    秋风的笔记
  • TypeScript 、React、 Redux和Ant-Design的最佳实践

    Peter谭金杰
  • 干货 | React Hook的实现原理和最佳实践

    React的组件化给前端开发带来了前所未有的体验,我们可以像玩乐高玩具一样将组件堆积拼接起来,组成完整的UI界面,在加快开发速度的同时又提高了代码的可维护性。

    携程技术
  • Kotlin —  最佳实践

    code_horse
  • Kafka最佳实践

    作者:Sriharsha Chintalapani, Jay Kumar SenSharma 译者:java达人 来源:https://community.ho...

    java达人
  • Impala最佳实践

    https://blog.cloudera.com/blog/2017/02/latest-impala-cookbook/

    Fayson
  • PHP最佳实践

    虽然名字叫《PHP最佳实践》,但是它主要谈的不是编程规则,而是PHP应用程序的合理架构。

    ruanyf
  • jQuery最佳实践

    上周,我整理了《jQuery设计思想》。 那篇文章是一篇入门教程,从设计思想的角度,讲解"怎么使用jQuery"。今天的文章则是更进一步,讲解"如何用好jQue...

    ruanyf
  • Kafka 最佳实践

    这是一篇关于 Kafka 实践的文章,内容来自 DataWorks Summit/Hadoop Summit(Hadoop Summit)上的一篇分享,里面讲述...

    王知无-import_bigdata
  • jQuery最佳实践

    jQuery的版本更新很快,你应该总是使用最新的版本。因为新版本会改进性能,还有很多新功能。

    用户2192970
  • Dubbo 最佳实践

    服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。

    JavaEdge

扫码关注云+社区

领取腾讯云代金券