React Hook

React Hook是React16.8.0引入的。使可以在不引入class的情况下,可以使用state和其他React特性。

hooks本质上是一些函数。

1. 为什么引入Hook?

1. hooks中的useEffect可以解决class中各逻辑在生命周期函数中管理混乱的问题。

2.hooks中的自定义Hook使得可以不修改组件的结构的基础上,灵活的复用组件逻辑。

3.class组件不能很好的压缩,并且热重载不稳定。不利于组件优化。使用Hook的函数组件不存在这些问题

2. Hook的规则

1. 只能在函数组件中使用hooks

类组件中无效。

2. 只能在函数最外层使用

不能用于if,for等语句中,也不能用于普通js函数中。因为ReactHook通过调用顺序确定对应的state等对应的hook方法。使用语句等会改变顺序。

3. 只能在以名字use开头的自定义Hook中使用

3. useState

接受一个参数作为初始值,参数可以是常量,也可以是一个返回值的函数。

初始值如果是一个函数,在初次渲染执行;如果是一个函数的执行,每次渲染都会执行

//  初始值是一个函数
const [count, setCount] = useState(function init(){......;return ..})
// 初始值是一个函数调用
const [count, setCount] = useState(init())

以数组形式返回两个值,第一个是状态值(初次渲染创建变量),一个是改变状态的函数。

例如:

const [count, setCount] = useState(0);
// 0是初始值

修改状态的函数(如setCount)和class中的setState类似,可以接受两种参数:

setCount(fn/exp)

1)表达式

可以是常量, 也可以是一个带state值的表达式

<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(count+1)}>-</button>

2)函数

<button onClick={() => setCount(prevCount => prevCount + 1)}>-</button>

⚠️如果修改后的状态不变,重复调用只会刷新一次。

<button onClick={() => setCount(count)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> 
<!-- 每次单击第一个按钮会渲染一次,继续单击不继续渲染;
       但是如果再单击第二个按钮,切换到第一个按钮,还是会渲染一次-->

模拟getDerivedStateFromProps

在render前进行setState更新

应用:在异步函数中获取state的值

在setTimeout等异步函数中获取的状态值,是调用setTimeout方法时的状态值,不是执行时的状态值。

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

  function handleAlertClick() {
    // count的取值形成了一个Example的闭包,每次刷新都是一个新的闭包
    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>
  );
}
ReactDOM.render(<Example/>, window.root);

4. useEffect

它相当于是componentDidMount,componentDidUpdate,componentWillUnMount三个生命周期的合成体。

它接受两个参数, 第一个是一个函数(effect),第二个参数可选,是一个数组(第一个函数中用到的可变数据集合)。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新;每次渲染,第一个函数都相当于一个新生成的函数

1. 执行时机

和生命周期不同的是,componentDidMount,componentDidUpdate是DOM渲染完成,屏幕更新前触发执行,会阻碍屏幕渲染;useEffect中的effect函数(非变更DOM的操作)在浏览器完成布局和绘制后,会延迟执行(异步,但是肯定在下一次渲染前执行),不会阻碍屏幕更新。但是如果是变更DOM的操作,需要同步执行。

1) 如果只在初次加载的时候运行,模拟componentDIdMount

  useEffect(() => { 
    //TODO
  }, []);

2)想要只在更新的时候运行,模拟componentDidUpdata

//使用useRef()存储的实例变量作为是否是第一次执行的标识
  const [count, setCount] = useState(0);
  const first = useRef(true);
  useEffect(() => { 
    if (first.current) {
      first.current = false;
    } else {
      console.error(count);
      document.title = `You clicked ${count} times!`;
    }
  }, [count]);

2. 执行副作用

如果副作用需要取消,在传入useEffect的函数中返回一个函数,在返回的函数中执行取消操作,该返回函数会在组件卸载的时候执行。

 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 发生变化时,重新订阅

3. 性能优化

第二个参数传入的变量,会作为比较的依据。如果变量值不变,就会跳过本次副作用的执行。

作为参数的值是在组件域(即函数组件对应的函数域)中的可变量(props/state),而且在useEffect中使用。

5. useContext

使函数组件可以具有和Class组件中的contextType属性(使可以通过this.context访问值)一样的功能。

用法和Class中contextType基本一致。接受一个context对象作为参数,返回最近的Provider提供的值。

示例代码:

const ThemeContext = React.createContext('dark');
function App() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Child />
      <button onClick={() => setTheme(preTheme => preTheme === 'dark' ? 'light' : 'dark')}>切换主题</button>
    </ThemeContext.Provider>
  );
}
function Child() {
  const value = useContext(ThemeContext);
  return (
    <div>{value}</div>
  );
}

6.useReducer

可以看做useState的复合版。当应用中需要多个状态时,一般需要调用多次useState(便于组合逻辑)。随着应用逐渐扩展,会越来越复杂,此时可以使用useReducer。

const [state, dispatch] = useReducer(reducer, initialArg, init);
// 第三个值可以不传,是一个接受第二个参数,返回一个初始值的函数

示例:

import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';

function App() {
  function reducer(state, action) {
    switch(action.type) {
    case 'add':
      return state + 1;
    case 'minuse':
      return state - 1;
    case 'reset':
      return 0;
    default: 
      throw new Error('error');
    }
  }
  const [count, dispatch] = useReducer(reducer, 0);
  return (
    <div>
      <p>You clicked {count} times!</p>
      <button onClick={() => dispatch({type: 'reset'})}>Reset</button>
      <button onClick={() => dispatch({type: 'add'})}>+</button>
      <button onClick={() => dispatch({type: 'minuse'})}>-</button>    
    </div>
  );
}

模拟forceUpdate()

function Example() {
  const [count, forceUpdate] = useReducer(x=>x+1, 0);
  return (
    <div>
      <button onClick={() => forceUpdate()}>Click me</button>
    </div>
  );
}

7. useMemo

本质上是memorization技术。用于优化在渲染阶段,计算数据的函数。

它接受一个计算函数作为第一参数;第二个是个依赖项数组。

返回一个值,该值是第一个计算函数的返回结果。

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

示例:

import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import React, {useState, useMemo, useEffect} from 'react';

function App() {
  const inputRef = React.createRef();
  const [data, setData] = useState([]);
  useEffect(() => {
    inputRef.current.value = null;
  });
  return ( 
    <div>
      <input type='text' ref={inputRef}/>
      <button onClick={() => setData(data.concat(inputRef.current.value))}>添加</button>
      <Child data={data}/>
    </div>
  );
}
function Child(props) {
  const [filterText, setFilterText] = useState('');
  const lists = useMemo(() => props.data.filter(i => i && i.includes(filterText)), [filterText, props]);
  return(
    <div>
      <input onChange={(e) => setFilterText(e.target.value)} />
      <ul>
        {
          lists.map((i,inx) => <li key={inx}>{i}</li>)
        }
      </ul>
    </div>
  );
}
Child.propTypes = {
  data: PropTypes.array
};

ReactDOM.render(<App/>, window.root);

8. useCallback

和 useMemo基本相同;

不同点在于它返回一个函数,这个和'memorize-one'的功能相同。

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

代码示例:

// 用useCallback替换上面useMemo的示例
  const listFilter = useCallback(() => props.data.filter(i => i && i.includes(filterText)), [filterText, props]);
  const lists = listFilter(filterText, props);

该方法比useRef的优点是,因为返回一个函数,它更容易抽取逻辑,形成自定义Hook

function MeasureDOM() {
  const [rect, measureRef] = useClientRec();
  return (
    <>
      <h1 ref={measureRef}>Hello, world</h1>
      {rect && <h2>The above header is {Math.round(rect.height)}px tall</h2>}
    </>
  );
}
function useClientRec() {
  const [rect, setRect] = useState();
  // 此时ref属性指向一个回调函数
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, ref];
}

9 .useRef

useRef模拟的是Class组件中的实例属性。可以在函数组件中很方便的保存任何可变值,不引起渲染。

// initialValue是current属性的初始值
const refContainer = useRef(initialValue);

使用useRef和React.createRef的区别是:

1)useRef返回一个refContainer对象({current: XXX}),并且重新渲染,访问的都是同一个对象。

2)React.createRef在函数组件中是每次重新渲染,相当于重新创建了一个对象,每次都是一个新对象。

示例:

// 单击一次后,inputRef1为1, inputRef2仍然为0
import React, {useState,useEffect, useRef} from 'react';
import ReactDOM from 'react-dom';

function App() {
  const [count, setCount] = useState(0);
  const inputRef1 = useRef(null);
  const inputRef2 = React.createRef();
  useEffect(() => {
    if(count === 1) {
      inputRef1.current.value = count;
      inputRef2.current.value = count;
    }
  }, [count,inputRef1,inputRef2]);
  return (
    <div>
      <input ref={inputRef1} /><br/>
      <input ref={inputRef2} /><br/>
      inputRef1:{inputRef1.current && inputRef1.current.value || 0}<br/>
      inputRef2:{inputRef2.current && inputRef2.current.value || 0}<br/>
      <button onClick={() => setCount(count+1)}>Add</button>
    </div>
  );
}

ReactDOM.render(<App/>, window.root);

应用: 获取prevState或者prevProps

// 考虑到通用性,将其提取到自定义Hook中usePrevious
function App() {
  const [count, setCount] = useState(0);
  const previous = usePrevious(count);
  return (
    <div>
      <p>previous: {previous}</p>  
      <p>current: {count}</p>  
      <button onClick={() => {setCount(count+1);}}>Add</button>
    </div>
  );
}
// 自定义Hook-获取prevState/prevProps
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

10. useImperativeHandle

该方法可以自定义(默认是DOM节点)子组件暴露给父组件的内容。

useImperativeHandle(ref, createHandle, [deps])

该方法基于Refs转发,需要和React.forwardRef结合使用。

示例:

import React, { useRef,useEffect, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';

function App() {
  const inputRef = useRef(null);
  useEffect(() => {
    // 在父组件查看获取到的子组件中传递的ref的内容
    console.log(inputRef); // {current: {focus: () => {...}}}
  });
  return (
    <div>
      <FancyInputWithRef ref={inputRef} />
    </div>
  );
}
// 子组件
function FancyInput(props, ref) {
  // 自定义ref暴露的内容
  useImperativeHandle(ref, () => ({
    focus: () => ref.current.focus()
  }), [ref]);
  return(
    <div>
      <input ref={ref} />
    </div>
  );
}
// 转发ref
var FancyInputWithRef = React.forwardRef(FancyInput);

ReactDOM.render(<App/>, window.root);

11. useLayoutEffect

和useEffect功能基本相同。

主要区别是它的执行时机和componentDidMount,componentDidUpdate相同,都在浏览器绘制之前执行。

建议: 先使用useEffect, 有问题再用useLayoutEffect

12. useDebugValue

用于调试时在自定义Hook中添加标签。

useDebugValue(date, date => date.toDateString());

示例:

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

  // ...

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

  return isOnline;
}