React Hooks

引言

从React 16.8版本开始,新增的React hooks可以让用户在不用创建react class的同时可以使用state等react属性;

本文部分内容翻译自React Hooks官方文档


什么是hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components. Hooks don’t work inside classes — they let you use React without classes. (We don’t recommend rewriting your existing components overnight but you can start using Hooks in the new ones if you’d like.)


React为什么引入hooks?

  1. 引入hooks可以将很多之前以类的形式表现的组件以函数的形式呈现,也不必从React中引入Component,不用再使用冗余的class语法,代码结构更加简洁;

  2. 引入hooks之后,可以将Reactstateprops的形式抽离出去,需要的时候再使用hooks将这些外部功能引入,能够使得组件尽量以纯函数的形式呈现,从而提高了组件的复用性;

  3. 简化了部分结构,比如多个子组件共享状态,使用hooks可以更好地进行状态共享,而不是像之前给多个子组件分别以props的方式进行传值;

  4. useState等hooks,新引入的hooks用于函数式组件中,不用再关心this指向,this绑定问题及由此带来的各种bug,同时代码更加精简;

    React Conf 2018上指出,提出React hooks出于以下三个原因:

    1. 组件间复用状态逻辑较难:即使有render propsHOC,这也需要你重新组织你的组件结构,并且使代码变得难理解和维护;并且由providersconsumers、高阶组件、render props等组成的组件会形成嵌套地狱。所以,react需要为共享状态逻辑提供更好的原生途径
    2. 复杂组件变得难以理解:class组件的逻辑需要用到很多生命周期函数,如componentDidMountcomponentDidUpdatecomponentWillUnmount等;当组件变得庞大时,会有很多状态逻辑和副作用;使用class组件时很多相关的逻辑会拆分到不同的生命周期函数中,而不相关的一些逻辑又会放在同一个方法中;这样逻辑不一致也变得难以维护,因为修改某个逻辑往往要查看多个地方的相关联逻辑;
      Hook不会强制根据生命周期进行划分,可以将组件相关联的部分拆分为更小的函数,便于逻辑统一与管理;
    3. 难以理解的class:class组件需要时刻注意this的指向问题,需要进行事件绑定等,这增加了很多冗余代码;另外,class组件不能很好的压缩, 也会出现HMR不稳定的问题;
      React组件一直更象函数。而Hook则拥抱了函数,同时也没牺牲React的精神原则


hooks并没有突破性的改变

根据官方文档,引入hooks的React新版本时向后兼容的,并且不会对用户之前对React建立的理解和使用造成困扰。另外,官方目前并没有将class移出React的计划,因此是否学习React Hooks—— It depends on you!

React Hooks使用了JavaScript闭包机制,因此可以直接访问函数组件中的stateprops



hooks规则

  1. 只在React函数组件中使用这些hooks;
  2. 只在最顶层调用hooks函数,不能在循环,条件语句及嵌套函数中调用;

hooks本质

每个组件内部会维护一个hooks的列表,当列表中的一个hook被使用后,指针就会移动到下一个位置;因此需要保证Hooks的调用规则:只在顶层调用;不能在条件、循环、嵌套函数中调用;
React Hooks_第1张图片



常用hooks介绍

State Hook ——useState

  1. 介绍
    useState是React新引入的hook(钩子函数),可以在函数式的组件中调用,用于设置和更新状态,作用类似于之前版本中的this.statethis.setState()useState接受一个state的初始值作为参数;返回值为一个state和一个设置state的函数,通常以setstate命名;这样,useState将状态值的初始化、读取及更新集于一身。
  2. 如果管理多个状态值,需要多次调用useState
    const [ count, setCount ] = useState(0);
    const [ color, setColor ] = useState('red');
    
  3. demo
    // counter demo
    import React, {
            useState } from 'react';
    import ReactDOM from 'react-dom';
    
    const App = () => {
           
    	const [ count, setCount ] = useState(0);	// 设置state.count的初始值为0;
    	return <button onClick={
           () => setCount(count + 1)}>{
           count}</button>
    }
    
    

Effect Hook —— useEffect

  1. 介绍:根据官方文档介绍,数据获取、事件订阅、DOM操作等可能会影响到其他组件并且不能在组件渲染过程中进行,这些操作被称之为side-effectsEffectsuseEffect钩子函数让我们可以在函数式组件中引入这些副作用,作用等同于class中使用的一些生命周期函数的,如componentDidMountcomponentDidUpdatecomponentWillUnMount。 默认情况下,React在每次render后会运行Effects;由于事件订阅(如定时器)等如果运行Effects之后不及时清理,会造成内存泄漏。基于此,Effects被分为需要清理的Effects不需要清理的Effects
    不需要清理的Effects:此时useEffect等效于componentDidMount + componentDidUpdate
    需要清理的Effects:此时useEffect等效于componentDidMount + componentDidUpdate + componentWIllUnMount

  2. 特点:由于React的生命周期函数中并没有一个函数可以让我们在每次render之后执行某些操作,使用class要达到这样的目的,需要我们使用至少两个生命周期函数componentDidMountcomponentDidUpdatecomponentDidMount仅在第一次渲染后执行,componentDidUpdate则是在第一次挂载后更新时才会执行);

    使用useEffect之后,逻辑更加清晰,很多操作不用再分析到底是要在componentDidMount还是componentDidUpdate中执行;

    useEffect通常在React的函数式组件中调用,这样可以直接使用stateprops等;

    useEffect接收一个函数作为参数,该参数为我们想要在render之后进行的操作,即所谓的Effects

    componentDidMountcomponentDidUpdate不同,useEffect不会阻塞浏览器更新页面;

    useEffect中的副作用函数每次都会被重新派发,从而避免被停滞;

    当使用需要清理的Effects时,useEffect可以返回一个函数,该函数中可以定义如何清理副作用。react会在需要清理该副作用时自动调用该函数;

    使用多个useEffect可以实现关注点分离:传统的class中生命周期函数常常包含不相关的逻辑,而相关的逻辑又分布在不同的生命周期函数中,使用多个useEffect可以解决这一问题;

  3. 性能
    有些操作我们只想在componentDidMount执行一次,而useEffect会每次渲染都执行。类似的情况很多,而跳过不必要的Effects可以节省性能开销。
    在react类组件中,我们在使用componentDidUpdate时可以使用以下方法:

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

    useEffect接受第二个数组形式的参数传入上一个state值,使用useEffect则可以使用以下方法:

    useEffect(() => {
           
    	document.title = `我会加倍奉还:${
             count * 2}次`;
    }, [count])
    

    如果第二个参数传入空数组,则表示该组件卸载时才进行解绑操作;

  4. demo

    // counter demo
    import React, {
            useState, useEffect } from 'react';
    import ReactDOM from 'react-dom';
    
    useEffect(() => {
           
    	document.title = `我会加倍奉还: ${
             count*2}`;
    })
    const App = () => {
           
    	const [ count, setCount ] = setState(0);	// 设置state.count的初始值为0;
    	return <button onClick={
           () => setCount(count + 1)}>{
           count}</button>
    }
    
    
    /*
    * 如果effect频繁变化怎么办?
    */
    // 错误示例
    function Counter() {
           
    	const [count, setCount] = useState(0);
    	
    	useEffect(() => {
           
    		const id = setInterval(() => {
           
    			setCount(count+1);	// 由于Hook的执行时创建一个闭包,count初始值一直为0;count的值永远不会超过1;
    		}, 1000);
    		return () => clearInterval(id);
    	}, []); // Bug: `count`未指定为依赖,只会在初始化时执行;指定count依赖能修复这个bug,但会导致每次更新时定时器会被重置。
    }
    
    // 推荐方案
    function Counter() {
           
    	const [count, setCount] = useState(0);
    	
    	useEffect(() => {
           
    		const id = setInterval(() => {
           
    			setCount(c => c + 1); // 使用setState的函数式更新方式,函数方式也可以避免重新创建`useState`的初始值;
    		}, 1000);
    		return () => clearInterval(id);
    	}, []); 
    }
    
    

    你可以从依赖中去除dispatchsetState,和useRef包裹的值,React会确保它们是静态的


Data Sharing Hook —— useContext

使用context我们可以为一些子组件共享数据,并避免通过中间组件传值;所有的后代组件都可以使用共享数据;

  1. 介绍
  2. API
  3. demo
    代码参见codesandbox.io
    import React, {
            useContext } from "react";
    import ReactDOM from "react-dom";
    
    const themes = {
           
      light: {
           
        background: "#eee",
        foreground: "#000"
      },
      dark: {
           
        background: "#000",
        foreground: "#fff"
      }
    };
    const ThemeContext = React.createContext(themes.light); // 传入默认值
    
    const App = () => {
           
      return (
        <ThemeContext.Provider value={
           themes.dark}>
          <ToolBar />
        </ThemeContext.Provider>
      );
    };
    
    const ToolBar = () => {
           
      return (
        <div>
          <ThemeButton />
        </div>
      );
    };
    
    const ThemeButton = () => {
           
      const theme = useContext(ThemeContext);
      return <button style={
           {
           color: theme.foreground, backgroundColor: theme.background}}>Styled by theme context.</button>;
    };
    
    ReactDOM.render(<App />, document.getElementById("root"));
    
    

useReducer

useReducer功能与useState功能类似,但比其功能更加强大。如果你有复杂的状态逻辑需要处理,如下一个状态值依赖于先前的状态值,那么使用useReducer是比useState更好的选择。另外,对于多层嵌套传值的情况,使用useReducer可以dispatch代替callbacks进行性能优化。

useReducer可以进行状态解耦,将useEffect和一些非强相关的状态解耦出来,更新逻辑由reducer统一处理;

This is why I like to think of useReducer as the “cheat mode” of Hooks. It lets me decouple the update logic from describing what happened. This, in turn, helps me remove unnecessary dependencies from my effects and avoid re-running them more often than necessary.

译:这就是为什么我倾向于认为useReducerHooks的“作弊模式”,它能够让我讲更新逻辑和描述发生了什么进行解耦。这样转而可以帮助我移除effects中不必要的依赖,并且避免很多必要的重复执行。

  1. 使用方式

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

    其中参数reducerredux中的概念,是一个纯函数,根据action.type返回新的state
    第二个参数是一个包含初始值的对象;
    第三个参数可选,是用于初始化的函数,参数为第二个参数;
    如果使用前两个参数,那么初始值是固定的,如果使用三个参数,那么初始值可以通过外部传入;

  2. demo

    demo演示见codesandbox.io;

    传入useReducer第三参数的demo演示见codesandbox.io

    import React, {
            useReducer } from 'react';
    import ReactDOM from 'react-dom';
    
    const initialState = {
           count: 0}
    
    const reducer = (state, action) => {
           
      let newState = JSON.parse(JSON.stringify(state));
      switch (action.type) {
           
        case 'increment':
          newState.count = state.count + 1;
          break;
        case 'decrement':
          newState.count = state.count - 1;
          break;
        default:
          break;
      }
      return newState;
    }
    
    const App = () => {
           
      const [ state, dispatch ] = useReducer(reducer, initialState);
      return (
        <div>
          <button onClick={
           () => dispatch({
           type: "increment"})}>increment</button>
          <span style={
           {
           padding: "5px"}}>{
           state.count}</span>
          <button onClick={
           () => dispatch({
           type: "decrement"})}>decrement</button>
        </div>
      );
    }
    
    
    ReactDOM.render(<App/>, document.getElementById('root'));
    

获取DOM及保存变量 —— useRef

  1. 用法

    const refContainer = useRef(initialValue);
    

    useRef返回一个可变对象,它的current属性的初始值为initialValue,该对象在组件的整个生命周期中均保持不变;

    useRef的本质就是可以在其.current属性中保存可变值的一个盒子,可以保存任何可变值;变更.current不会引发组件重新渲染;

  2. demo
    demo1地址

    // ref获取DOM, demo地址https://codesandbox.io/s/blissful-burnell-22wmc
    import React, {
            useRef } from "react";
    
    const Demo = () => {
           
      const inputEl = useRef(null);
      const onButtonClick = () => {
           
        inputEl.current.focus();
      };
      const onButtonClick2 = () => {
           
        inputEl.current.value = "input changed";
      };
      return (
        <>
          <input ref={
           inputEl} />
          <button onClick={
           () => onButtonClick()}>Focus</button>
          <button onClick={
           () => onButtonClick2()}>Change Input</button>
        </>
      );
    };
    
    export default Demo;
    
    
    // demo2 —— 定时器清理
    
    function Timer() {
           
    	const intervalRef = useRef();
    	
    	useEffect(() => {
           
    		const id = setInterval(() => {
           
    			// ...
    		});
    
    		intervalRef.current = id;
    		
    		return () => {
           
    			clearInterval(intervalRef.current);
    		};
    	}, []);
    }
    

    如果只是想设定一个循环计时器,其实不需要ref也可以的,本身在useEffectid一直存在的;
    但是,如果想要在一个事件处理器中清除这个循环定时器的话;使用useRef就很有用了

    function hnadleCancelClick() {
           
    	clearInterval(intervalRef.current);
    }
    

useContext结合useReducer代替redux

demo具体见codesandbox.io,源自技术胖react hooks文档


缓存状态 —— useMemo,缓存方法 —— useCallback

useMemo通常返回一个memoized值,入参是一个函数,返回的是函数执行结果并缓存,仅在变更时才更新值;通常用语缓存计算量较大的函数结果;

useMemo的入参函数实在渲染期间运行的,所以不要在 useMemo中做一些通常不会在渲染期间做的事;

useCallback通常返回一个memoized函数,仅在函数的某个依赖发生变化时才会更新;当我们需要将函数传递下去并且在子组件的useEffect中调用它时,可以使用useCallback

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

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


// demo2——父组件向子组件传递函数
function Parent() {
     
  const [query, setQuery] = useState('react');

  // ✅ Preserves identity until query changes
  const fetchData = useCallback(() => {
     
    const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
    // ... Fetch data and return it ...
  }, [query]);  // ✅ Callback deps are OK

  return <Child fetchData={
     fetchData} />
}

function Child({
      fetchData }) {
     
  let [data, setData] = useState(null);

  useEffect(() => {
     
    fetchData().then(setData);
  }, [fetchData]); // ✅ Effect deps are OK

  // ...
}

构建自己的Hooks

当我们需要在两个JS函数之间共享逻辑时,可以使用自定义的hook函数,从而避免编写过多的重复代码;自定义的hook其参数返回值等都可以自定义,并且其内部可以调用其他的React Hooks;自定义的hook均以use作为函数名前缀;

自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。

demo参见codesandbox.io

import React, {
      useState, useEffect, useCallback } from "react";
import "./styles.css";

const useWinSize = () => {
     
  const [size, setSize] = useState({
     
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });

  const onResize = useCallback(() => {
     
    setSize({
     
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    });
  }, []);

  useEffect(() => {
     
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, [onResize]);

  return size;
};

const Demo = () => {
     
  const size = useWinSize();
  return <div>页面size:{
     `width:${
       size.width}, height: ${
       size.height}`}</div>;
};

export default function App() {
     
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <Demo />
    </div>
  );
}


Q & A

  1. React如何知道哪个state对应哪个useState?
    A: 通过保证Hook在每次渲染中都是相同的调用顺序;因此不能在条件、循环等场景使用React Hooks且必须要在顶层调用;

  2. 如何有条件地执行Effect
    A: 可以在Hook内部最顶层加入判断;

  3. 如何测量DOM节点(获取DOM节点位置或者大小等信息)
    A:使用callback ref —— 如果想要在react绑定或者解绑DOM节点的ref时运行某些代码,可以使用callback ref

    function MeasureExample() {
           
    	const [height, setHeight] = useState(0);
    	
    	const measureRef = useCallback(node => {
           
    		if (node !== null) {
           
    			setHeight(node.getBoundingClientRect().height);
    		}
    	}, []);
    	
    	return (
    		<>
    			<h1 ref={
           measureRef}>Hello, world</h1>
    			<h2>The above header is {
           Math.round(height)}px tall</h2>
    		</>
    	);
    }
    
  4. Class迁移到Hook,生命周期方法如何对应

    • construtor: 函数组件不需要,初始化state使用useState;对于初始化计算量大的操作,可以传一个函数给useState
    • shouldComponentUpdate: 借助React.memoReact.memo更类似与PureComponent,它只会对props进行浅比较(不会比较state
    • componentDidMountcomponentDidUpdatecomponentWillUnmount:使用useEffect可涵盖;
    • render:即函数组件本身;
  5. 获取上一轮propsstate的值
    可以通过ref手动实现(demo来自react官网文档),实现代码可参考此处

    function Counter() {
           
    	const [count, setCount] = useState(0);
    
    	const prevCountRef = useRef();
    	useEffect(() => {
           
    		prevConutRef.current = count; // useEffect发生在渲染后,所以可以拿到最新值;
    	});
    	const prevCount = prevCountRef.current;	 // 保存更新前的count
    
    	return <div> Now: {
           count}, before: {
           prevCount} </div>
    }
    

    抽象为自定义hook

    function Counter() {
           
    	const [count, setCount] = useState(0);
    	const prevCount = usePrevious(count);
    	return <div> Now: {
           count}, before: {
           prevCount} </div>
    }
    
    function usePrevious(value) {
           
    	const ref = useRef();
    	useEffect(() => {
           
    		ref.current = value;
    	});
    	
    	return ref.current;
    }
    
  6. 惰性创建昂贵的对象

    • 场景一:初始state创建昂贵时,使用函数初始化
      const [pos, setPos] = useState(createPos(props.count);
      
      // 创建代价高时的推荐写法
      const [pos, setPos] = useState(() => createPos(props.count);
      
    • 场景二:避免重新创建useRef的初始值
      useRef不像useState能够接受一个特殊的函数重载,需要自己编写函数
      function Image(props) {
               
      	const ref = useRef(null);
      
      	// IntersectionObserver只会被创建一次
      	function getObserver() {
               
      		if (ref.current === null) {
               
      			ref.current = new IntersectionObserver(onIntersect);
      		}
      		return ref.curren;
      	}
      	
      	// 然后在需要时,调用getObserver()
      }
      
  7. Hook 会因为在渲染时创建函数而变慢吗?

    不会。在现代浏览器中,闭包类的原始性能只有在极端场景下才会有明显的差别。

    除此之外,可以认为 Hook 的设计在某些方面更加高效:


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

  8. 如何避免向下多层传入回调:
    可以使用useReducer向下传递一个dispatch函数;

  9. stateuseEffect的区别?
    state的每次渲染总是获取到最新的状态值,而不是当次渲染对应的特定值;而hooks每次渲染都有自己的effect,每个effect中获取的总是当次渲染对应的stateprops

    effect会在每次渲染之后才运行,并且概念上它是组件输出的一部分,可以“看到”某次特定渲染的propsstate

    每一个组件内的函数(包括事件处理函数、effects、定时器或者API调用等等)会捕获某次渲染中定义的propsstate

    以下是demo对比:

    function Counter() {
           
    	const [count, setCount] = useState(0);
    	
    	useEffect(() => {
           
    		setTimeout(() => {
           
    			console.log(`You clicked ${
             count} times`);
    		}, 3000);
    	});
    	
    		return (
    			<div>
    				<p>You clicked {
           count} times</p>
    				<button onClick={
           () => setCount(count + 1)}>Click me</button>
    			</div>
    		);
    }
    // 输出结果为0/1/2/3/4/5,其中0是首次渲染的结果;
    
    /**
    * state/componentDidMount/componentDidUpdate
    */
    componentDidUpdate() {
           
    	setTimeout(() => {
           
    		console.log(`You clicked ${
             count} times`);
    	}, 3000);
    }
    

    // 输出结果0 5 5 5 5 5,因为state总是获取的是最新状态值;


  1. useEffect的清理时机
    由于每次渲染都有对应的effect,所以useEffect会在下次运行Effect之前清理之前渲染的effect
    参考如下代码理解useEffect的运行与清理:

    useEffect(() => {
           
    	ChatAPI.subscribeToFriendStatus(props.id, handleStatussChange);
    	return() => {
           
    		ChatAPI.unsubscribeToFriendStatus(props.id, handleStatusChange);
    	};
    });
    

    假如第一次渲染时props是{id: 10},第二次渲染时是{id: 20},你可能认为:

    • React清除了{id: 10}的effect;
    • React渲染了{id: 20}的UI;
    • React运行{id: 20}的effect

    而实际上,如上所说,useEffect会在下次运行Effect之前清理之前渲染的effect,其执行过程如下:

    • React渲染了{id: 20}的UI;
    • React清除了{id: 10}的effect;
    • React运行{id: 20}的effect
  2. useEffect中的函数依赖处理(参考A Complete Guide to useEffect)

    1. 如果定义的函数仅会在effect中调用,那么可以吧函数直接定义在effect内部。这样我们不需要再去关心那些间接依赖;
    2. 函数不能直接定义在effect中——比如函数是从props中获取的,或者函数逻辑在多个effect中可以复用:
      1. 如果一个函数不依赖组件内的任何值,那么可以把函数定义到组件之外,然后可以自由地在effect中使用;另外,你不需要将其设置为依赖,因为其不在渲染范围内,不引用propsstate,不受数据流影响;
        function getFetch(query) {
                   
        	return 'http://caniuse.com?query=' + query;
        }
        
        function SearchResults() {
                   
          useEffect(() => {
                   
            const url = getFetchUrl('react');
            // ... Fetch data and do something ...
          }, []); // ✅ Deps are OK
        
          useEffect(() => {
                   
            const url = getFetchUrl('redux');
            // ... Fetch data and do something ...
          }, []); // ✅ Deps are OK
        
          // ...
        }
        
      2. 可以使用useCallback将函数进行包裹——**useCallback的本质是添加了一层依赖检查,使得函数本身只在需要的时候才改变,而不是去掉对函数的依赖。**这样可以避免在渲染范围内的函数每次都执行。
        function SearchResults() {
                   
          // ✅ Preserves identity when its own deps are the same
          const getFetchUrl = useCallback((query) => {
                   
            return 'https://hn.algolia.com/api/v1/search?query=' + query;
          }, []);  // ✅ Callback deps are OK
        
          useEffect(() => {
                   
            const url = getFetchUrl('react');
            // ... Fetch data and do something ...
          }, [getFetchUrl]); // ✅ Effect deps are OK
        
          useEffect(() => {
                   
            const url = getFetchUrl('redux');
            // ... Fetch data and do something ...
          }, [getFetchUrl]); // ✅ Effect deps are OK
        
          // ...
        }
        

参考文献

  1. https://reactjs.org/docs/hooks-overview.html
  2. A Complete Guide to useeffect
  3. https://reactjs.org/docs/hooks-reference.html#usecontext
  4. Hooks FAQ
  5. A Complete Guide to useEffect

你可能感兴趣的:(web前端,javascript,react,react,hooks,useState,context,函数式组件)