React Hooks 实用指南

6,399 阅读9分钟

前言

React Conf 2018会议中,Dan Abramov 介绍了 React Hooks。官方的描述为

Hook是一项新功能提案,可让您在不编写类的情况下使用状态和其他React功能。 它们目前处于React v16.7.0-alpha中。计划将在 2019 Q1 推出到主版本中。

痛点

以下是React Hooks功能的动机,它解决了现有React中的一些问题

组件之间很难共享状态

React没有一种将可重用的行为附加到组件的方法(例如链接到store)。如果您使用过一段时间,您可能会使用render propsheight-order components组件解决这个问题。但是这些模式要求您再使用他们时重构组件,这会很麻烦。如果您使用React DevTools看一下您的程序,您会发现您的组件被各种组件所包裹这叫做包装地域,比如:providers、comsumers、higher-order components、render props等。这里有一个更深层的根本问题:React需要一个更好的方法来共享状态逻辑。

这就是Hooks,您可以从组件中提取有状态逻辑,以便可以独立测试和重用。Hooks允许您不更改组件层次结构的情况下重用有状态逻辑。这样就可以轻松在多组件之间或与社区共享Hooks

组件越来越复杂,变得难以理解

我们经常不得不维护一些组件,这些组件一开始很简单,随着时间的延伸组件发展成一堆无法管理的有状态逻辑和一些副作用。每个生命周期方法经常包含不相关的逻辑组合。举个例子,组件可能会在componentDidMountcomponentDidUpdate中拉取一些数据。还有componentDidMount方法可能还包含一些事件监听的不相关逻辑,并且再componentWillUnmount`中卸载监听。但是完全不相关的代码会合并到一个方法中。是很容易引起bug和不一致性。

在很多情况下,不能将这些组件拆分成更小的组件因为逻辑遍布许多地方。对它们进行测试也很困难。这正是很多人将React和状态管理库结合使用的原因。但是这更容易创建更多的抽象,要求您在许多不同的文件之间跳转,重用组件将变得更加困难。

为了解决这个问题,Hooks允许您根据相关的功能将他们拆分为一个更小的函数。而不是强制基于声明周期函数进行拆分。您还可以选择使用reducer管理组件的本地状态,使其更具可预测性。

类让人和机器都混淆

除了使代码重用和代码组织更加困难外,我们发现类(classes)可能成为学习React的一大障碍。您必须了解它在JavaScript中是如何工作的,这与它在大多数语言中的工作方式有很大不同。您必须明白如何正确的绑定事件处理和还没稳定的新语法,代码非常冗长。大家可能很容易就会明白属性(props)、状态(state)、从上往下的数据流(top-down data flow)但类(classes)就很难理解。React中的函数和类组件之间的区别以及何时使用每个组件导致即使在经验丰富的React开发人员之间也存在分歧。使用函数可以使用prepack更好的优化代码。但是使用类组件不能得到更好的优化。

为了解决这些问题,Hooks 允许您在没有类的情况下使用更多的React功能。

useState

useState可以让您的函数组件也具备类组件的state功能

使用语法如下:

const [state, setState] = useState(initialState);

useState返回一个数组,一个是state的值,第二个是更新state的函数

在真实的程序中我们可以这样使用:

function TestUseState() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>useState api</p>
      <p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
    </div>
  )
}

使用 useState 需要注意一个事项,当你初始化是一个对象时。使用 setCount 时它不像类组件的 this.setState 会自动合并到 state 中。setCount 会使用当前的值覆盖之前的 state。如下所示

function TestUseStateObject() {
  const [state, setState] = React.useState({
    count: 0,
    greeting: "Hello, World!",
  });
  const handleAdd = () => {
    setState({
      count: state.count + 1
    })
  }
  console.log('state > ', state)
  return (
    <div>
      <p>useStateObject api</p>
      <p>Count: {state.count} <button onClick={handleAdd}>自增</button></p>
    </div>
  )
}

1543423038609

我们可以看到,当点击按钮时 state 被替换成了 {count: 1}。如果想要在 state 中使用一个对象需要在更新值的时候把之前的值解构出来,如下所示:

setState({
      ...state,
      count: state.count + 1
    })

在函数中使用多个 state

function TestMultipleUseState() {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState('john');
  return (
    <div>
      <p>useState api</p>
      <p>Count: {count} - Name: {name}</p>
    </div>
  )
}

如需要在线测试请前往codepen useState

useEffect

默认情况下 useEffect 在完成渲染后运行,我们可以在这里获取DOM和处理其他副作用。但它还有两种不同的运行阶段稍候我会解释。

function TestUseEffect() {
  const [count, setCount] = React.useState(0);
  
  React.useEffect(() => {
    console.log(`组件被更新,Count: ${count}`);
  });
  
  return (
    <div>
      <p>useEffect api</p>
      <p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
    </div>
  )
}

上面的 useEffect 在每次组件渲染后运行,每当我们点击自增按钮都会执行一次。

但是如果上面的代码在每次渲染后都执行,如果我们在 useEffect 从服务器拉取数据。造成的结果就是每次渲染后都会从服务器拉取数据。或者是只有某些 props 被更新后才想执行 useEffect。那么默认的 useEffect 就不是我们想要执行方式,这时 useEffect 提供了第二个参数。

useEffect(didUpdate, [])

useEffect第二个参数为一个数组。当我们提供第二个参数时,只有第二个参数被更改 useEffect 才会执行。利用第二个参数我们可以模拟出类组件的 componentDidMount 生命周期函数

function TestUseEffectListener() {
  const [count, setCount] = React.useState(0);
  
  React.useEffect(() => {
    console.log('componentDidMount fetch Data...');
  }, []);
  
  return (
    <div>
      <p>TestUseEffectListener</p>
      <p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
    </div>
  )
}

上面的代码中 useEffect 只会执行一次,当您点击自增 useEffect 也不会再次执行。

useEffect 第一个参数的函数中我们可以返回一个函数用于执行清理功能,它会在ui组件被清理之前执行,结合上面所学的知识使用 useEffect 模拟 componentWillUnmount 生命周期函数

function TestUseEffectUnMount() {
  const [count, setCount] = React.useState(0);
  
  React.useEffect(() => {
    return () => {
      console.log('componentUnmount cleanup...');
    }
  }, []);
  
  return (
    <div>
      <p>TestUseEffectUnMount</p>
    </div>
  )
}

上面的代码中,当组件 TestUseEffectUnMount 将要销毁时会,会执行 console.log('componentUnmount cleanup...') 代码

如需要在线测试请前往codepen useEffect

useContext

useContext 可以让您在函数中使用 context,它有效的解决了以前 ProviderConsumer 需要额外包装组件的问题

使用语法如下:

const context = useContext(Context);

现在让我们来看看实际应用中这个 useContext 是如何使用的,代码如下:

function TestFuncContext() {
  const context = React.useContext(ThemeContext);

  return (
    <div style={context}>TestFuncContext</div>
  )
}

我们可以看到上面直接使用 React.useContext(ThemeContext) 就可以获得 context,而在之前的版本中需要像这样才能获取 <Consumer>({vlaue} => {})</Consumer> ,这极大的简化了代码的书写。

// 之前Consumer的访问方式
function TestNativeContext() {
  return (
    <ThemeContext.Consumer>
      {(value) => {
        return (
          <div style={value}>TestNativeContext</div>
        )
      }}
    </ThemeContext.Consumer>
  );
}

如需要在线测试请前往codepen useContext

useReducer

useReduceruseState 的代提方案。当你有一些更负责的数据时可以使用它。

使用语法如下:

const [state, dispatch] = useReducer(reducer, initialState)

第一个参数是一个 reduce 用来处理到来的 action,函数申明为:(state, action) => ()。第二个参数是一个初始化的state常量。

在返回值 [state, dispatch] 中,state 就是你的数据。dispatch 可以发起一个 action 到 reducer 中处理。

这个功能给我的感觉就是组件本地的redux,感觉还是不错。在设计一些复杂的数据结构是可以使用

现在让我们来看看实际应用中这个 useReducer 是如何使用的,代码如下:

function TestUseReducer() {
  const [state, setState] = React.useReducer((state, action) => {
    switch(action.type) {
      case 'update':
        return {name: action.payload}
      default:
        return state;
    }
  }, {name: ''});
  
  const handleNameChange = (e) => {
    setState({type: 'update', payload: e.target.value})
  }
  return (
    <div>
      <p>你好:{state.name}</p>
      <input onChange={handleNameChange} />
    </div>
  )
}

当改变 input 中的值时会同时更新 state 中的数据,然后显示在界面上

如需要在线测试请前往codepen useReducer

useCallback

useCallbackuseMemo 有些相似。它接收一个内联函数和一个数组,它返回的是一个记忆化版本的函数。

使用语法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a), [a])

useCallback 的第一个参数是一个函数用来执行一些操作和计算。第二个参数是一个数组,当这个数组里面的值改变时 useMemo 会重新执行更新这个匿名函数里面引用到 a 的值。这样描述可能有点不太好理解,下面看一个例子:

function TestUseCallback({ num }) {
  const memoizedCallback = React.useCallback(
    () => {
      // 一些计算
      return num;
    },
    [],
  );
  console.log('记忆 num > ', memoizedCallback())
  console.log('原始 num > ', num);
  return (
    <div>
      <p>TestUseCallback</p>
    </div>
  )
}

_6d371a9a-47a9-4a5c-ac22-28a456b4d4d5

如果我们想监听 num 值的更新重新做一些操作和计算,我们可以给第二个参数放入 num 值,像下面这样:

function TestUseCallback({ num }) {
  const memoizedCallback = React.useCallback(
    () => {
      // 一些计算
      return num;
    },
    [num],
  );
  console.log('记忆 num > ', memoizedCallback())
  console.log('原始 num > ', num);
  return (
    <div>
      <p>TestUseCallback</p>
    </div>
  )
}

如需要在线测试请前往codepen useCallback

useRef

我觉得 useRef 的功能有点像类属性,或者说您想要在组件中记录一些值,并且这些值在稍后可以更改。

使用语法如下:

const refContainer = useRef(initialValue)

useRef 返回一个可变的对象,对象的 current 属性被初始化为传递的参数(initialValue)。返回的对象将持续整个组件的生命周期。

一个保存input元素,并使其获取焦点程序,代码如下:

function TestUseRef() {
  const inputEl = React.useRef(null);
  const onButtonClick = () => {
    // 点击按钮会设置input获取焦点
    inputEl.current.focus(); // 设置useRef返回对象的值
  };
  
  return (
    <div>
      <p>TestUseRef</p>
      <div>
        <input ref={inputEl} type="text" />
        <button onClick={onButtonClick}>input聚焦</button>
      </div>
    </div>
  )
}

useRef 返回的对象您可以在其他地方设置比如: useEffect、useCallback等

如需要在线测试请前往codepen useRef

原文链接

感谢阅读 🙏

最后做一个广告,我创建了一个前端周刊每周五发布最新的技术文章和开源项目欢迎订阅