【React】668- React Hooks的优缺点

最后都变了- 提交于 2021-01-12 07:49:52
https://zhihu.com/people/zhan-zi-zhen-84

下面我谈一下我认为的react hooks的优缺点,优缺点通过和传统的React.Component进行对比得出。

优点

一、更容易复用代码

这点应该是react hooks最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。hooks是怎么解决这个复用的问题呢,具体如下:

每调用useHook一次都会生成一份独立的状态,这个没有什么黑魔法,函数每次调用都会开辟一份独立的内存空间。虽然状态(from useState)和副作用(useEffect)的存在依赖于组件,但它们可以在组件外部进行定义。这点是class component做不到的,你无法在外部声明state和副作用(如componentDidMount)。上面这两点,高阶组件和renderProps也同样能做到。但hooks实现起来的代码量更少,以及更直观(代码可读性)。

举个例子,我们经常使用的antd-table,我们经常需要写一些状态:pagination={current:xxx,pageSize:xxx,total:xxx}。但许多场景只是简单的表格,我们希望封装一个高阶组件,自带这些状态,并可以自动调用server去获取remote data

用高阶组件来实现这样它:

import { Table } from 'antd'
import server from './api'
@useTable(server)
class App extends Component{
  render(){
    // useTable和tableProps的代码是分离的,但高阶组件一多,代码会变得较难阅读,
   // 你难以区分这个props是来自哪个高阶组件,或者还是来自业务的父组件。
    const { tableProps } = this.props;
    return (
      <Table 
        columns={[...]}
      // tableProps包含paginationonChangedataSource等属性
        {...tableProps}
      />

    )
  }
}

用hooks来实现的话,会是:

import { Table } from 'antd'
import server from './api'
function App {
    const { tableProps } = useTable();
    return (
      <Table 
        columns={[...]}
      // tableProps包含paginationonChangedataSource等属性
        {...tableProps}
      />

    )
}
/*
相对比高阶组件“祖父=>父=>子”的层层嵌套,
hooks是这样的:  
const { brother1 } = usehook1; 
const { brother2} = usehook2;
*/

实现有以下几点优势:

把"useTable"和输出的值写到一起,结构更清晰,更容易阅读和维护。对比高阶组件,代码量更少。

二、清爽的代码风格

函数式编程风格,函数式组件、状态保存在运行环境、每个功能都包裹在函数中,整体风格更清爽,更优雅。另外,对比类组件,函数组件里面的unused状态和unused-method更容易被发现。

三、代码量更少

向props或状态取值更加方便,函数组件的取值都从当前作用域直接获取变量,而类组件需要先访问实例引用this,再访问其属性或者方法,多了一步。更改状态也变得更加简单, this.setState({ count:xxx })变成 setCount(xxx)。因为减少了很多模板代码,特别是小组件写起来更加省事,人们更愿意去拆分组件。而组件粒度越细,则被复用的可能性越大。所以,hooks也在不知不觉中改变人们的开发习惯,提高项目的组件复用率。

缺点

一、响应式的useEffect

写函数组件时,你不得不改变一些写法习惯。你必须清楚代码中useEffectuseCallback等api的第二个参数“依赖项数组”的改变时机,并且掌握上下文的useEffect的触发时机。当逻辑较复杂的时候,useEffect触发的次数,可能会被你预想的多。对比componentDidmountcomponentDidUpdateuseEffect带来的心智负担更大。

二、状态不同步

这绝对是最大的缺点。函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面,当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。

import React, { useState } from "react";
const Counter = () => {
  const [counter, setCounter] = useState(0);
  const onAlertButtonClick = () => {
    setTimeout(() => {
      alert("Value: " + counter);
    }, 3000);
  };
  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button onClick={() => setCounter(counter + 1)}>Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 3 seconds
      </button>
    </div>

  );
};

export default Counter;

当你点击Show me the value in 3 seconds的后,紧接着点击Click me使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。

这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxxthis.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。

其实解决这个hooks的问题也可以参照类的instance。用useRef返回的immutable RefObjectcurrent属性是可变的)来保存state,然后取值方式从counter变成了:counterRef.current。如下:

import React, { useState, useRef, useEffect } from "react";

const Counter = () => {
  const [counter, setCounter] = useState(0);
  const counterRef = useRef(counter);
  const onAlertButtonClick = () => {
    setTimeout(() => {
      alert("Value: " + counterRef.current);
    }, 3000);
  };
  useEffect(() => {
    counterRef.current = counter;
  });
  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button onClick={() => setCounter(counter + 1)}>Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 3 seconds
      </button>
    </div>

  );
};

export default Counter;

结果如我们所期待,alert的是当前的值1。

我们可以把这个过程封装成一个custom hook,如下:

import { useEffect, useRef, useState } from "react";
const useRefState = <T>(initialValue: T):
 [T, React.MutableRefObject<T>, 
   React.Dispatch<React.SetStateAction<T>>] => {
  const [state, setState] = useState<T>(initialValue);
  const stateRef = useRef(state);
  useEffect(() => {
    stateRef.current = state;
  }, [state]);
  return [state, stateRef, setState];
};

export default useRefState;

尽管这个问题被巧妙地解决了,但它不优雅、hack味道浓,且丢失了函数编程风格。

怎么避免react hooks的常见问题

  1. 不要在 useEffect里面写太多的依赖项,划分这些依赖项成多个单一功能的 useEffect。其实这点是遵循了软件设计的“单一职责模式”。
  2. 如果你碰到状态不同步的问题,可以考虑下手动传递参数到函数。如:
// showCount的count来自父级作用域 
const [count,setCount] = useState(xxx); 
function showCount()
  console.log(count) 


// showCount的count来自参数 
const [count,setCount] = useState(xxx); 
function showCount(c)
  console.log(c) 
}

但这个也只能解决一部分问题,很多时候你不得不使用上述的useRef方案。

  1. 重视eslint-plugin-react-hooks插件的警告。

  2. 复杂业务的时候,使用Component代替hooks

感想

目前,我通常更偏向于用hooks来写组件,但在复杂业务中,我会更倾向于用class Component或者两者结合的方式。hooks会是未来的主流组件编写方式,但目前来说它还不成熟。



1. JavaScript 重温系列(22篇全)
2. ECMAScript 重温系列(10篇全)
3. JavaScript设计模式 重温系列(9篇全)
4.  正则 / 框架 / 算法等 重温系列(16篇全)
5.  Webpack4 入门(上) ||  Webpack4 入门(下)
6.  MobX 入门(上)  ||   MobX 入门(下)
7.  70+篇原创系列汇总

回复“加群”与大佬们一起交流学习~

点击“阅读原文”查看70+篇原创文章

点这,与大家一起分享本文吧~

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!