《React Hook》指北

Hook 是React 16.8版本中新增的内容,它的出现让函数组件拥有了类似class组件的能力(生命周期等),同时也优化了一些过去的缺点,例如:

  • Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。
  • 符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

useState

useState 是允许你在 React 函数组件中添加 state 的 Hook。它很像class组件中的this.setState方法。

首先引入 React 中 useState 的 Hook,然后在函数组件的顶部声明state变量 和 更新state变量的方法

useState 接收一个initialValue,它是一个state变量的初始值, 其类型可以是一个对象、数字、字符串或者其他都可以。

一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留, 有点闭包的感觉喔。


import React, { useState } from 'react';

function Example() {
  // 声明一个叫 “count” 的 state 变量
  const [count, setCount] = useState(0);
}

useState 方法的返回值

当前 state 以及更新 state 的函数,你需要成对的获取它们。所以我们可以使用数组解构的方式成对获取,这样更加的方便和具有语义化。
它并不难懂,就像这样:

var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
var fruit = fruitStateVariable[0]; // 数组里的第一个值
var setFruit = fruitStateVariable[1]; // 数组里的第二个值

读取State

在函数中,我们可以直接用 count:

  <p>You clicked {count} times</p>

更新State

<button onClick={() => setCount(count + 1)}>
  Click me
</button>

注意

不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它。

useEffect

Effect Hook 可以让你在函数组件中执行副作用操作: 数据获取,设置订阅以及手动更改 React 组件中的 DOM
你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合, 所以在有些时候,使用函数组件会比class组件更加方便。

但与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。

import React, { useState, useEffect } from 'react';

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

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

使用

useEffectuseState一样,可以多次使用

useEffect接收2个参数:

  • 第一个参数是一个函数:fn, 如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它(clean up function),相当于React会自动的在componentWillUnmount帮我们执行它
  • 第二个参数是一个数组:[var, ...]

    • 可以省略,如果省略的话那么useEffect 会在第一次渲染之后和每次更新之后都会执行。
    • 或是一个空数组:只会在第一次渲染(mount)之后时执行一次,从此后不再执行,这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。
    • 当然很多情况下是一个有值的数组[props, data, count,...],数组中的值代表副作用依赖的一些变量,如果这些值中有任意一个发生变化,那么useEffect会重新执行

有清除副作用的useEffect

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 指定清理函数,当然并不是必须为 effect 中返回的函数命名。
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

通过跳过 Effect 进行性能优化,在组件卸载时有条件的执行清理函数

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。
在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

对于有清除操作的 effect 同样适用:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

在 useEffect 中调用用函数时,要把该函数在 useEffect 中申明,不能放到外部申明,然后再在 useEffect 中调用

因为这样你可能会漏掉某些依赖项,而且在直觉上你是无法直接看出到底使用了哪些依赖,可能导致useEffect执行时机出现问题

// bad
function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }

  useEffect(() => {
    doSomething();
  }, []); //  这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}

// good
function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }

    doSomething();
  }, [someProp]); //  安全(我们的 effect 仅用到了 `someProp`)
}

useLayoutEffect

官方提示我们:尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useLayoutEffectuseEffect 的作用以及使用方式是相似的,只是在触发的时机上有不同。

  • useEffect 在全部渲染完毕后才会执行, 也就是DOM树和CSSOM树全部绘制完毕之后DIsplay才会执行,这个时候页面已经展示完毕了
  • useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行

我们可以使用它来读取 DOM 布局并同步触发重渲染,也就是说可以使用它做一些干预DOM的事情,但不可否认的是,它会阻塞视觉更新,让页面展示慢了一点。

使用


useLayoutEffect = () => {
  // do something...
}

useContext

useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
它只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context

在class组件中,通过使用React.creatContext()方式创建一个Context并指定Context value,在其子组件下,挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 __this.context__ 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

在函数组件中,使用__useContext(CONTEXT)__, CONTEXT必须是React.createContext创建出来的context 对象本身

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

// 创建一个顶层上下文Context,用于子节点获取。themes.light是一个默认值
const ThemeContext = React.createContext(themes.light); 

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

何时更新

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext providercontext value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

这个机制和class组件一致。

useReducer

听这个api的名字,感到熟悉了吗?是的,就像是Redux似的。

/*
  reducer: fn
  initialArg: 指定初始值
  init 可以省略: fn, 会自动的传入initialArg, init(initialArg)
*/
const [state, dispatch] = useReducer(reducer, initialArg, init);

它可以看做是useState的替代方案,像Redux一样提供 数据管理 的方式, 但是Redux是全局的,useReducer是局部的。

接收一个形如 (state, action) => newState 的 reducer,返回当前的state 以及 配套的 dispatch 方法。

如果state逻辑复杂且包含多个子值,当你使用useState重新赋值时会感到非常麻烦,又要保存之前不必更改的部分,又要修改你需要修改的地方,这很让人头疼...

使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。

使用

  • 指定初始值

    ...
    const initialState = {count: 0};
    
    function reducer(state, action) {
      switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
          return {count: state.count - 1};
        default:
          throw new Error();
      }
    }
    
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
      return (
        <>
          Count: {state.count}
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
      );
    }
    ...
  • 惰性初始化
    也就是:使用第三个init函数对initialArg进行某些操作
function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>

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

useCallback

useCallback 是一种__调优__手段,返回一个memoized回调__函数__,只有当依赖项发生改变时,这个函数才会真的执行。

使用

const memoizedCallback = useCallback(() => {
    doSomething(a, b);
  },[a, b]);

把内联回调函数doSomething()及 依赖数组[a, b]作为参数传入useCallback,它将返回该回调函数的记忆化版本,该回调memoizedCallback仅在某个依赖项改变时财辉更新。

当把memoizedCallback传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

子组件就可以在某些地方(例如 shouldComponentUpdate)里根据某些条件去判断是否执行以更新

__PS__:

  • 依赖项数组不会作为参数传给回调函数。
  • useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

使用useCallback 去测量一个盒子的大小 而不是useRef

这是因为当ref挂载的DOM发生改变时,useRef不会notify 通知你,使用useCallback 实现这个需求,因为每当 ref 被附加到一个另一个节点,React 就会调用 callback。

function MeasureExample() {
  const [height, setHeight] = useState(0);
  // 这有点类似类组件中的 回调refs
  const measuredRef = useCallback(node => { 
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

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

__注意__: 我们传递了 [] 作为 useCallback 的依赖列表。
这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

useMemo

useMemo 是一种__调优__手段,返回一个memoized回调__值__。

使用

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

把“创建”函数和依赖项数组作为参数传入useMemo,当依赖项更改时才会重新计算memoized值,如果依赖数组[a, b]自上次赋值以后没有改变过,useMemo 会跳过第二次调用,只是简单复用它上一次返回的值。

传入 useMemo 的函数会在渲染期间执行,所以建议在这个函数内部执行和渲染有关系的操作,比如页面渲染某个数值,而你又会在useMemo中进行计算等操作。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

注意

先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。不要过度耦合才是关键。

useMemo 本身也有开销。
useMemo 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。
这个过程本身就会消耗一定的内存和计算资源。因此,__过度使用 useMemo 可能会影响程序的性能__。

useRef

ref: referrence, 参考、引用
useRef() Hook 不仅可以用于 DOM refs。「ref」 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

useRef 返回一个可变的ref对象,其.current属性被初始化为传入的参数(initialValue)。

注意:返回的ref对象在组件的整个生命周期内保持不变,所以如果你把ref对象作为依赖项传入useEffect的依赖数组里,那这个hook就不会再次执行了

const refContainer = useRef(initialValue);

使用

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
  • 使用useRef创建一个ref对象(inputEl),初始值可以设为null
  • 在DOM中某个元素通过自定义属性 ref={inputEl} 进行挂载
  • 在需要使用的地方通过inputEl.current 对DOM元素进行访问

聊一下类组件的 创建Refs 和 回调Refs

见缝插针的聊一下

创建Refs

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef(); // 创建Ref
  }

  handleOnClick = () => {
    console.log(this.myRef.current); // 访问Ref
  }

  render() {
    return <div onClick={this.handleOnClick} ref={this.myRef} />; // 挂载Ref
  }
}

这是React在 v16.3中引入的新API,可以声明式的创建一个Ref对象,然后通过在DOM处使用ref 自定义属性挂载这个Ref对象,然后你就可以通过this.RefObject.current 进行访问。

ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例,但是可以使用useRef

回调Ref,帮助你更精细的控制Ref挂载和卸载

DOM的ref属性接收一个函数,传入当前DOM节点作为参数,你可以在这个函数内把DOM节点赋值到某个变量上,以供其他时机使用

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => { // 接收当前DOM节点作为参数
      this.textInput = element; // 赋值到this.textInput上
    };

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 组件挂载后,让文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
    // 实例上(比如 this.textInput)
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
          // ref={(node)=>{ this.node = element; }} // 一般情况下我们会直接简化成这样
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

forwardRef

顾名思义:转发Ref对象,以供操作

在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空

只有当它被赋给某个元素的 ref 属性时,才会有值

所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了

那么父组件就可以操作子组件中的某个元素

但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的

所以就需要用到 forwardRef 进行转发

  • forwardRef 可以在父组件中操作子组件的 ref 对象
  • forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
  • 子组件接受 props 和 ref 作为参数

使用:


function Child(props,ref){  // 子组件接收2个参数:props、ref
  return (
    <input type="text" ref={ref}/>
  )
}
// 子组件通过forwardRef进行再次包裹,你可以想象它是一个高阶组件的形式
Child = React.forwardRef(Child);  

function Parent(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();  // 父组件创建一个空的Ref对象 {current:''}
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/> // 将Ref对象通过传值得方式挂载到子组件上
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

Hooks 对应的类组件声明周期

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState
  • getDerivedStateFromProps:改为 在渲染时 安排一次更新。
  • shouldComponentUpdate:详见 React.memo.
  • render:这是函数组件体本身。
  • componentDidMount, componentDidUpdate, componentWillUnmountuseEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
  • componentWillUnmount:相当于 useEffect 里面返回的 cleanup 函数
  • getSnapshotBeforeUpdatecomponentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

使用Hooks 请求数据

如果要使用Hooks 请求数据,那么请求数据的函数需要放置在 useEffect中,这是因为useEffect是一个专注副作用的钩子函数。

它的执行时机相当于class组件中的componentDidMountcomponentWillUpdatecomponentWillUnmount 的组合体。

但是在useEffect中请求数据,我们需要一下:

默认情况下,请求数据的过程可能会执行多次(mount/update),甚至会有进入死循环,获取数据 => state变化 => 更新dom => 导致useEffect再次执行=> loop loop

// bad
import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data); //  如果每次拿到的data不同,则会陷入死循环
  });


  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

// good
...

useEffect(async () => {
  const result = await axios(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );

  setData(result.data);
}, []); // 这样,只会在组件mount时执行一次,update时就不更新了

...

但是这样还有一个问题,如果我们使用的是async/await的方式,那么它默认是会返回一个promise的,但是对于useEffect来说,它应该是什么都不返回的,或者返回一个clean up函数,用于清除副作用(组件unmount时会执行)。

 useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {  // 清除函数,避免内存泄漏等问题
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

所以我们在之前的那一步async/await 会出现一个error:
`index.js:1452 Warning: useEffect function must return a cleanup function or nothing.
Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.`

知晓了这个原因,那我们可以改变思路,只要不让useEffect 直接返回promise就可以了:

...

useEffect(() => {
  
  const fetchData = async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  }

  fetchData();
}, []);

...

如何使用Hook避免向下层层传递回调

在组件树中,通过手动的给每一层手动传递回调是一件非常让人头疼的事情。

所以,React官方推荐我们 通过context 用 useReducer往下传一个dispatch函数,然后子组件使用dispatch函数向上传递一个action来更改存储的state

  • father component

    const TodosDispatch = React.createContext(null);
    
    function TodosApp() {
      const [todos, dispatch] = useReducer(todosReducer);
    }
    
    return (
      <TodosDispatch.Provider value={dispatch}>
        <DeepTree todos={todos} />
      </TodosDispatch.Provider>
    )
  • grandson component01, 层级很深的某个嵌套组件

    function DeepChild(props) {
      const dispatch = useContext(TodoDispatch); // 使用 useContext 传入context对象 生成dispatch方法
    
      function handleClick() {
        dispatch({ type: 'add', text: 'hello' }); // dispatch 一个action 触发 reducer中的更改规则
      }
    
      return (
        <button onClick={handleClick}>Add todo</button>
      )
    }

-同理:grandson component 02, 层级很深的某个嵌套组件

function DeepComp(props) {
  const dispatch = useContext(TodoDispatch); // 使用 useContext 传入context对象 生成dispatch方法

  function handleClick() {
    dispatch({ type: 'decrement', text: 'running' }); // dispatch 一个action 触发 reducer中的更改规则
  }

  return (
    <button onClick={handleClick}>decrement todo</button>
  )
}

HOOK FAQ

接下来 可能需要再细看一些比较具体的场景了

那么请移步官方文档吧:https://react.docschina.org/docs/hooks-faq.html

OR

终于搞懂 React Hooks了!!!!!

蚂蚁保险体验技术 - 写React Hooks前必读

React Hooks 详解 【近 1W 字】+ 项目实战

30分钟精通React Hooks

10分钟教你手写8个常用的自定义hooks

Hooks 的性能优化及可能会遇到的坑总结


1 + 1 =

求知若飢,虛心若愚。