前端修仙路-React Hooks

React Hooks

前言:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。


生命周期如何对应到Hook?

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。
  • getDerivedStateFromProps:改为 在渲染时 安排一次更新。
  • shouldComponentUpdate: 使用React.memo(component)函数包裹即可。
  • render: 函数组件体本身就是个render
  • componentDidMount,componentDidUpdate, componentWillUnmount: useEffect Hook 可以表达所有这些。
  • getSnapshotBeforeUpdate,componentDidCatch,getDerivedStateFromError:目前还没有这些对应hooks写法,后续版本会添加

Hooks API 概览

  • 基础 Hook
    • useState
    • useEffect
    • useContext
  • 额外的 Hook
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue
  • 自定义 Hook

Hooks 规则

  1. 不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。

  2. 不要在普通的 JavaScript 函数中调用 Hook。你可以:
    • 在 React 的函数组件中调用 Hook
    • 在自定义 Hook 中调用其他 Hook

react官方发布了一个eslint-plugin-react-hooks的eslint插件来检测规则。后续版本可能会集成到cli中。

    npm install eslint-plugin-react-hooks --save-dev
{
                    //你的 ESLint 配置
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
     
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

State Hook

eg

import React, {
      useState } from 'react';    //引入useState函数

function component() {
     
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);    //返回了2个元素的数组值,第一个是state,第二个是改变state的方法

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

state 初始值为 { count: 0 },调用setCount(count+1)就可以实现状态更新。

Effect Hook

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

eg:

import React, {
      useState, useEffect } from 'react'; //引入useEffect函数

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

  useEffect(() => {
             //componentDidMount 和 componentDidUpdate  会调用
    function handleStatusChange(status) {
     
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    //每次 重新渲染 时都会被调用,以清除副作用代码, 比如定时器
    return function cleanup() {
      
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

state初始值{isOnline:null},friend.id改变时在线状态会进行重置,cleanup会在组件销毁时清除订阅,以消除副作用代码。useEffect 默认会在调用一个新的 effect 之前对前一个 effect 进行清理,也就是组件重新渲染的时候就会进行清理,如果设置了return

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。只要传递数组作为 useEffect 的第二个可选参数即可:

eg:

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

小提示:如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。

Context Hook

如果你在接触 Hook 前已经对 context API 比较熟悉,useContext(MyContext) 就相当于 class 组件中的 static contextType = MyContext 或者

eg:

const themes = {
                 //声明全局state
  light: {
     
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
     
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light); //创建context组件,并设置默认值

function App() {
     
  return (
    <ThemeContext.Provider value={
     themes.dark}> //定义provider,传入主题对象
      <Toolbar />
    </ThemeContext.Provider>
  );
}

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

function ThemedButton() {
     
  const theme = useContext(ThemeContext);   //声明使用context
  return (
    <button style={
     {
      background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

自定义Hook

  • 自定义 Hook 必须以 “use” 开头,不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
  • 两个组件中使用相同的hook并不会共享state,自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
  • 在自定义hook中调用其他hook就如同在组件内部调用一样,从 React 的角度来看,我们的组件只是调用了 useState 和 useEffect等这些hook。
function useFriendStatus(friendID) {
             //声明自定义hook
  const [isOnline, setIsOnline] = useState(null);

  // ...

  return isOnline;
}

function FriendStatus(props) {
     
  const isOnline = useFriendStatus(props.friend.id);    //使用自定义hook

  // ...
}

function FriendListItem(props) {
     
  const isOnline = useFriendStatus(props.friend.id);    //使用自定义hook

  return (
    <li style={
     {
      color: isOnline ? 'green' : 'black' }}>
      {
     props.friend.name}
    </li>
  );
}

Reducer Hook

    const [state, dispatch] = useReducer(reducer, initialState , init);
  • reducer (state, action) => newState,一个reducer函数
  • initialState 初始化的state对象
  • init 参数可选,惰性地创建初始 state, 将用于计算 state 的逻辑提取到 reducer 外部
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);  //重置state为initialCount
    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>
    </>
  );
}

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

Callback Hook

仅当someState发生变更,才会返回新函数,否则返回之前的缓存函数。

  const memoizedCallback = useCallback(
  fn,
  [someState]
);

useCallback(fn, deps) 相当于 useMemo(() => fn, deps),useCallback和useMemo区别是,前一个返回缓存的函数,后一个返回缓存的变量值。

Memo Hook

类似于vue的computed,缓存一个函数的返回值,仅当依赖属性someState发生变更才更新这个计算值。

  const computedValue = useMemo(
  ()=>computedValue,
  [someState]
);

eg:

function WithMemo() {
     
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    const expensive = useMemo(() => {
            //昂贵的性能开销操作,进行结果缓存
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
     
            sum += i;
        }
        return sum;
    }, [count]);
 
    return <div>
        <h4>{
     count}-{
     expensive}</h4>
        {
     val}
        <div>
            <button onClick={
     () => setCount(count + 1)}>+c1</button>
            <input value={
     val} onChange={
     event => setValue(event.target.value)}/>
        </div>
    </div>;
}

Ref Hook

//创建一个ref对象,ref.current属性初始化为initialValue值
const refContainer = useRef(initialValue);

eg:

function TextInputWithFocusButton() {
     
  const inputEl = useRef(null);         //创建ref对象
  const onButtonClick = () => {
     
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();    //通过inputEl代理操作dom
  };
  return (
    <>
      <input ref={
     inputEl} type="text" />   //将input的引用赋给inputEl
      <button onClick={
     onButtonClick}>Focus the input</button>
    </>
  );
}

ImperativeHandle Hook

ref透传场景时,使用ImperativeHandle Hook可以让父子组件拥有自己的ref,改变暴露给父组件的ref。

useImperativeHandle(ref, createHandle, [deps])
  • ref 不解释
  • createHandle 用来创建ref.current
  • deps 依赖的属性,仅当依赖属性变更时,才会重新渲染
import React, {
      useRef, useImperativeHandle } from 'react';

const FancyInput = React.forwardRef((props, ref) => {
        //ref透传
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
      //使父子组件的ref隔离开来,只能操作暴露的focus
    focus: () => {
     
      inputRef.current.focus();
    }
  }));

  return <input ref={
     inputRef} type="text" />
});

const App = props => {
     
  const fancyInputRef = useRef();

  return (
    <div>
      <FancyInput ref={
     fancyInputRef} />
      <button
        //此时使用的fancyInputRef.current是useImperativeHandle暴露的对象
        onClick={
     () => fancyInputRef.current.focus()} 
      >父组件调用子组件的 focus</button>
    </div>
  )
}

LayoutEffect Hook

effect hook一致,区别是它会在所有的 DOM 变更之后,在浏览器执行绘制之前,同步调用effect。也就是说会阻塞浏览器的绘制。推荐首先使用effect hook,性能更好。

DebugValue Hook

用于在 React 开发者工具中显示自定义 hook 的标签。

useDebugValue(value)
function useFriendStatus(friendID) {
     
  const [isOnline, setIsOnline] = useState(null);

  // ...

  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值,也就是说复用同一个hook的时候用来区分是哪个调的比较方便。


__@terryvvan__

你可能感兴趣的:(react,hooks,javascript,前端)