【React】React Hook解读

什么是React Hook?

Hook 是一个特殊的函数,它可以让你”钩入”React state及生命周期等特性的函数。例如,useState是允许你在React函数组件中添加state的Hook。

使用React Hook的目的是什么?

使用Hook其中一个目的就是要解决class中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到几个不同方法中的问题。

什么时候会用React Hook?

如果你在编写函数组件并意识到需要向其添加一些state,以前的做法是必须将其它转化为class。现在你可以在现有的函数组件中使用Hook。

React Hook的规则是什么?

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

什么是Hook的副作用?

之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。

通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。
Effect Hook 可以让你在函数组件中执行副作用操作。
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect 会在每次渲染后都执行吗?
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。
有的Hook是需要清理的(比如定时器),有的是不需要清理的的(比如数据获取)。需要清理的副作用,需要返回一个函数。

不需要清理的effect:

import React, { useState, useEffect } from 'react';
   function Example() {
        const [count, setCount] = useState(0);
        useEffect(() => {
            document.title = `You clicked ${count} times`;
       });

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

需要清理的effect:

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);
            // Specify how to clean up after this effect:
            return function cleanup() {
                   ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
            };
      }); 
     if (isOnline === null) {
        return 'Loading...';
      }
     return isOnline ? 'Online' : 'Offline';
 }

effect性能优化:

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

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

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

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

更多详细Hook API,详见:Hook API

常见Hooks API

useState

设置和改变state,代替原来的state和setState。

import React, {useState} from 'react';

function RenderStateInstance({initialState}) {
    const [count, setCount] = useState(initialState);

    return (
        <>
          Count: {count}<br />     
          <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
          <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
        </>
    );
}

useMemo

控制组件更新条件,可根据状态变化控制方法执行,优化传值。

import React, { useState, useMemo } from 'react'
function calcNumber(count) {
  console.log("cal重新计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total
}
export default function MemoHookDemo01() {
  const [count, setCount] = useState(10);
  const [show, setShow] = useState(true);
//   const total = calcNumber(count);
  const total = useMemo(() => {
    return calcNumber(count);
  }, [count])
  return (
    <div>
      <h2>计算数字的和:{total}</h2>
      <button onClick={e => { setCount(count + 1) }}>+1</button>
      <button onClick={e => { setShow(!show) }}>show切换</button>
    </div>
  )
}

这个例子中:

useMemo第一个参数为回调函数,第二个参数为依赖项;
当依赖项发生改变时,执行回调函数。

useCallback

useMemo优化传值,usecallback优化传的方法,是否更新。


import React, { useState, useCallback } from 'react';
 
const set = new Set();
 
export default function Callback() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
 
    const callback = useCallback(() => {
        console.log(count);
    }, [count]);
    set.add(callback);
 
 
    return <div>
        <h4>{count}</h4>
        <h4>{set.size}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;

}

使用场景:
有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。


import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
 
    const callback = useCallback(() => {
        return count;
    }, [count]);
    return <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}
 
function Child({ callback }) {
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>

useRef

跟以前的ref,一样,只是更简洁了。它可以用来获取组件实例对象或者是DOM对象。

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

export default function App(props){
  const [count, setCount] = useState(0);

  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);

  const couterRef = useRef();

  useEffect(() => {
    document.title = `The value is ${count}`;
    console.log(couterRef.current);
  }, [count]);
  
  return (
    <>
      <button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
    </>
  );
}

代码中用useRef创建了couterRef对象,并将其赋给了button的ref属性。这样,通过访问couterRef.current就可以访问到button对应的DOM对象。
而useRef这个hooks函数,除了传统的用法之外,它还可以“跨渲染周期”保存数据。

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

export default function App(props){
  const [count, setCount] = useState(0);

  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);

  const timerID = useRef();
  
  useEffect(() => {
    timerID.current = setInterval(()=>{
        setCount(count => count + 1);
    }, 1000); 
  }, []);
  
  useEffect(()=>{
      if(count > 10){
          clearInterval(timerID.current);
      }
  });
  
  return (
    <>
      <button ref={couterRef} onClick={() => {setCount(count + 1)}}>Count: {count}, double: {doubleCount}</button>
    </>
  );
}

useEffect

页面加载完成以后,想要做的一些操作,这个经常是componentDidMount里面做的一些事情,但是不会block browser painting。
代替原来的生命周期,componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版。

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

        useEffect(() => {
            setTimeout(() => {
                console.log(count)
            }, 3000)
        })
        // 在3秒内快速点击5次按钮,控制台打出的结果是什么样的?
        // 0 1 2 3 4 5
        return (
            <div>
                <p>You clicked {count} times</p>
                <button onClick={() => setCount(count + 1)}>
                    Click me
                </button>
            </div>
        );
    }

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useContext 共享状态钩子

createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。

import React, { useContext } from "react";
import ReactDOM from "react-dom";

const TestContext= React.createContext({});

const Navbar = () => {
  const { username } = useContext(TestContext)

  return (
    <div className="navbar">
      <p>{username}</p>
    </div>
  )
}

const Messages = () => {
  const { username } = useContext(TestContext)

  return (
    <div className="messages">
      <p>1 message for {username}</p>
    </div>
  )
}

function App() {
  return (
	<TestContext.Provider 
		value={{
			username: 'superawesome',
		}}
	>
		<div className="test">
			<Navbar />
			<Messages />
		</div>
	<TestContext.Provider/>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


useReducer():action 钩子

Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState。

useReducers()钩子用来引入 Reducer 功能。

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

上面是useReducer()的基本用法,它接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的dispatch函数。

下面是一个计数器的例子。用于计算状态的 Reducer 函数如下。

const myReducer = (state, action) => {
  switch(action.type)  {
    case('countUp'):
      return  {
        ...state,
        count: state.count + 1
      }
    default:
      return  state;
  }
}

组件代码如下。

function App() {
  const [state, dispatch] = useReducer(myReducer, { count:   0 });
  return  (
    <div className="App">
      <button onClick={() => dispatch({ type: 'countUp' })}>
        +1
      </button>
      <p>Count: {state.count}</p>
    </div>
  );
}

useImperativeHandle

useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用ref这样的命令式代码。useImperativeHandle应当与forwardRef一起使用。

ref和forwardRef

我们先来看下ref和forwardRef的使用。

  • 通过forwardRef可以将ref转发给子组件
  • 子组件拿到父组件创建的ref, 绑定到自己的某一个元素中
import React, { useRef, forwardRef } from 'react'

// forwardRef可以将ref转发给子组件
const JMInput = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />
})

export default function ForwardDemo() {
  // forward用于获取函数式组件DOM元素
  const inputRef = useRef()
  const getFocus = () => {
    inputRef.current.focus()
    console.log(inputRef.current);
  }

  return (
    <div>
      <button onClick={getFocus}>聚焦</button>
      <JMInput ref={inputRef} />
    </div>
  )
}

forwardRef的做法本身没有什么问题, 但是我们是将子组件的DOM直接暴露给了父组件:

  • 直接暴露给父组件带来的问题是某些情况的不可控
  • 父组件可以拿到DOM后进行任意的操作
  • 我们只是希望父组件可以操作的focus,其他并不希望它随意操作其他方法

比如上面的inputRef.current打印出来的就是一个DOM。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle使用简单总结:
作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
参数1: 父组件传递的ref属性
参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法

通过useImperativeHandle可以只暴露特定的操作。通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起,所以在父组件中, 调用inputRef.current时, 实际上是返回的对象

import React, { useRef, forwardRef, useImperativeHandle } from 'react'

const JMInput = forwardRef((props, ref) => {
  const inputRef = useRef()
  // 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
  // 参数1: 父组件传递的ref属性
  // 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    },
    test: () => {
        console.log('abcd');
    }
  }))
  return <input type="text" ref={inputRef} />
})

export default function ImperativeHandleDemo() {
  // useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
  // 为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
  // 解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多
  const inputRef = useRef();
  const action = () => {
    inputRef.current.focus();
    inputRef.current.test();
    console.log(inputRef.current);

  }

  return (
    <div>
      <button onClick={action}>聚焦</button>
      <JMInput ref={inputRef} />
    </div>
  )
}

所以上面inputRef.current返回的是一个对象:

useState 与useContext实例

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

const ThemeContext = React.createContext('light');

function RenderStateInstance({initialState}) {
    const [count, setCount] = useState(initialState);
    const contextType = useContext(ThemeContext);;

    return (
        <>
          Count: {count}<br />
          Theme: {contextType}<br /><br />
         
          <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
          <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
        </>
    );
}
 
 export default class Mine extends React.Component {
    

     render() {
        let initialState = 0;
         return (
            <ThemeContext.Provider value="dark">
                <div>
                    <p>My 页面 </p>
                    <RenderStateInstance   initialState={initialState} />
                </div>
            </ThemeContext.Provider>
        )
     }
 }

更多自定义Hooks

React 官网:https://zh-hans.reactjs.org/docs/hooks-custom.html
更多Github实例:https://github.com/ecomfe/react-hooks

自定义useTtitle

import { useEffect } from 'react';

const useTitle = title => {
    useEffect(() => {
        document.title = title;
    }, [])
    
    return;

};

export default useTitle;


使用useTitle:

import React from 'react';
import useTitle from '../hooks/useTitle';


const Title = () => {
    useTitle('修改Title')
    
    return (
        <>
            <p>这里用了自定义的Hooks title</p>
        </>
    );
};

export default Title;

自定义Hooks的规则

  • 自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
  • 自定义 Hook 必须以 “use” 开头
posted @ 2021-03-01 22:45  攀登高山  阅读(177)  评论(0编辑  收藏  举报