React Hooks 使用小结

748 阅读9分钟

React Hooks已经出来很久了,有人早已拥抱hooks,有人class大法如此好,何必hooks,也有人左手hooks,右手class。但是从掘金,知乎,博客以及面试等地方可以发现,hooks是无法避免的一个话题,好像不用hooks你就是上个世纪的人了(流下了前端卑微的泪水图片图片图片) 为了不当原始人,今天就来和大家一起探讨下hooks大法。

为什么要用hooks?

社区,论坛里各位大佬大多认为,更加的可抽象,逻辑可复用,代码精简,避免写各种生命周期,这里我就react在官方文档提到的hooks出现的动机(emmm...不就是好处么)与大家的使用感受做一下简单的总结:

  • 公共逻辑的复用。 在hooks出生之前,react没有将可复用性行为添加到组件到能力,针对这个问题广大开发者开始使用renderProps 或者 高阶组件来实现此目的。但是,说实话这些方法还是比较麻烦的,需要重新组织项目或者组件的组织结构。如果用react-devtools来观察我们的react应用,会发现代码结构中充满了providers,consumers,高阶组件,renderProps等非逻辑功能的抽象组件,这就形成来一个“嵌套地狱”。现在有了hooks,我们可以更方便的对公共逻辑进行抽离,更好的去复用,这个和react的组件化思想是一致的。

  • 精简代码。 react是以组件为核心,但是很多组件写着写着就成为了庞然大物,成为了巨石组件。我们常常会在componentDidMount 或者componentDidUpdate等生命周期中执行很多init以及update方法,同时很多还需要在componentWillUnmount里对很多方法或者timer进行清除。这就导致很多互相无关的逻辑混合到一起,并且清除工作可能有所遗漏。但是组件又不可能无限制的拆分为更小的颗粒,现在不用担心了,hooks 可以让我们对各个状态进行单独处理,单独清除,更加颗粒化的管理。

  • 远离this问题。react里我们常常使用.bind来处理this指向问题,当然后面我们用了es6的箭头函数帮助我们减少了.bind的工作量。不过这也说明了在class里this给大家带来的困扰。

说了这么多hooks多好处,那么hooks什么都好吗?当然不是,有些我们还是得借助class组件。

getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法目前还没有支持,不过官方表示,我们会加油,尽快支持到图片

体验

Hooks里的useState 会直接将data进行替换,而不是像class里setState一样进行合并,这个会带来点麻烦不过我们完全可以定义一个方法去实现,毕竟这也并不难。另外,react官方表示,你们不要弄复杂到对象作为useState到值,我们更加推荐颗粒度更小到data。如果不方便拆分,但是state又开始变得复杂了,怎么办? 推荐使用useReducer(useState的替代方案)或者自定义hook。

Hooks到一些使用注意点:

为什么我会在我的函数中看到陈旧的 props 和 state?例如下面的代码:

import React, {useState} from 'react';
function Example() {
  const [count, setCount] = useState(0);
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

export default Example;

如果我先点击 show alert按钮,然后立即点击 click me 将count加到5。 浏览器alert会弹出几?0! 并不是5,这是为啥? 因为组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。也就是方法执行对时候 已经把对应对state当时对值给记录下来了。 那么如果我们遇到这种类似对异步该怎么处理呢?使用ref来保存,修改和读取。 我们将代码修改如下:

function Example() {
    let countRef = useRef(0)
    const handleAlertClick=()=> {
      setTimeout(() => {
        alert('You clicked on: ' + countRef.current);
      }, 3000);
    }

  return (
    <div>
      <button onClick={() => countRef.current++}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

另外还有一种情况会让你看到陈旧的props或者state。就是我们在使用useEffect的时候,第二个参数传入了不恰当的依赖。 比如变化了username 但是我依赖的地方用了[]或者[age],这个时候我们要么直接移除依赖数组,要么传入恰当的依赖。 这里有一点就是之前思考这个ref不就像我在开始的地方定义了一个object变量么,为什么我不直接定义一个obj呢? 是因为每次re-render的时候 你定义的obj会被重新执行,这样每次拿到的都是初始值了。

如何获取上一轮的state?

使用ref!如下代码所示:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

可以函数组件引用函数组件吗?

不建议,但你可以通过 useImperativeHandle Hook 暴露一些命令式的方法给父组件。在如下代码中,渲染的父组件,可以通过inputRef.current.focus()来调用FancyInput内部的focus方法。

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

如何测量DOM节点?

涉及到DOM,首先会想到ref,但是一般在获取DOM大小及位置的时候我们更倾向于使用useCallback。每当一个ref被附加到一个节点的时候,react就会调用一次callback,如下示例:

function test(){
  const [height, setHeight] = useState(0);
  const measureRef = useCallback(node =>{
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
    // 这里采用[],只有组件加载卸载才会执行callback
  },[]) 

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  )
}

那么我们为什么不用useRef呢?因为当ref是一个对象的时候,useRef并不会把ref的变化通知给我们,但是使用callback的时候可以确保即便子组件是延迟显示的或者经过一定交互才会显示或者大小变化的情况下,我们依然能够在父组件接收到相关的信息,以便更新数据。

Hooks的实际使用

useState

useState的更新不是合并式更新,而是覆盖式更新,如下:

import React, { useState } from "react";
function Demo() {
  const [obj, setObject] = useState({
    count: 0,
    name: "zxf"
  });
  return (
    <div className="App">
      Count: {obj.count}
      <button onClick={() => setObject(
            { ...obj, count: obj.count + 1 }
          )}>+</button>
    </div>
  );
}

useEffect

useEffect第一个参数是一个函数,该函数返回一个函数,返回的函数是作为卸载时候的一个清除函数,类似于我们在componentDidMount里做的清除处理。 第二个参数,尽量不要使用空数组,除非该effect的确不收到其他props影响,而不是“你觉得”不需要。

useContext

useContext减少组件层级。在类组件中,我们要使用根元素传递来的数据(比如主题色)通常使用层层的props或者使用react.context,并使用provider包裹:

const { Provider, Consumer } = React.createContext(null); 使用useContext避免了Consumer层层嵌套,使用起来更加方便,代码阅读更加简介:

const themeContext = React.createContext("summer");

function Bar() {
  const theme = useContext(themeContext);
  return <div>{theme}</div>;
}

function Foo() {
  return <Bar />;
}

function App() {
  return (
    <themeContext.Provider value={"spring"}>
      <Foo />
    </themeContext.Provider>
  );
}

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init)

useReducer是useState的替代方案,它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

那么在那些情况下推荐使用useReducer?例如:state 逻辑较复杂且包含多个子值(我们上面提到,useState是覆盖式更新),或者下一个 state 依赖于之前的 state 等。使用useReducer来写下计数器示例:

const initialState = {count: 0}
const reducer = (state, action)=> {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

const Demo = ()=>{
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
      <>
        Count: {state.count}
        <button 
          onClick={() => dispatch({type: 'decrement'})}>
          -</button>

        <button 
          onClick={() => dispatch({type: 'increment'})}>
          +</button>
      </>
    )
}

react会确保dispatch标识的函数的稳定性,不会因为所在组件的重新渲染而产生改变。

tips:如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)

useCallback

useCallback返回一个记忆化的函数:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
我们在平时经常会写这样的代码:

function App() {
  const handleClick = () => {
    console.log('Click happened');
  }
  return 
  <SomeComponent onClick={handleClick}>Click</SomeComponent>
}

上述代码 即使什么都没有改动,但是每次页面渲染都时候 ,因为有传递函数所以都会重新渲染一遍。但是使用callback改写如下后:

function App() {
  const memoizedHandleClick = useCallback(() => {
    console.log('Click happened');
  },[])
  return 
  <SomeComponent onClick={memoizedHandleClick}>
      Click
  </SomeComponent>
}

它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useMemo记忆组件

useMemo和上面useCallback比较类似,区别在于useCallback会把第一个参数直接返回(不会执行),而useMemo会执行第一个参数。我们常常将自己的函数组件使用useMemo来包裹。

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。常用的方式为命令式的控制自组件:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus</button>
    </>
  );
}

有的时候会有疑问,这个ref不就是一个普通js对象么,那么我直接声明一个obj不就可以了?非也。useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。 如果是普通的对象,每次渲染的时候都会被初始化。

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref (即 useCallback)来实现。

这里提一下useState和useRef的小问题:如下代码当用户点击按钮之后 再在输入框中输入内容,3秒后message会是用户点击按钮时候的值还是更新后的值?

function MessageThread() {
  const [message, setMessage] = useState('');

  const showMessage = () => {
    alert('You said: ' + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

结果会弹出input修改之前的值。

在函数组件内会弹出之前的值,这个是React Hooks 中 Capture Value 特性。但是如果改用ref就会拿到最新的值。

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值,并通常与与 forwardRef 一起使用:

function ChildInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(ChildInput);

渲染 的父组件可调用 inputRef.current.focus():

function App() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  return (
    <div>
      <ChildInput ref={inputRef} />
    </div>
  );
}

最后推荐一下hooks的eslint插件:

eslint-plugin-react-hooks. This ESLint plugin enforces the Rules of Hooks. It is a part of the Hooks API for React.

我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

over