React自从16.8版本减少Hooks概念以来,其老版官网文档对于Hooks的解释是基于类组件来类比的,很容易让人感觉Hooks是不一样写法的类组件实现。目前React出了一份全新的官网文档,齐全基于Hooks与函数式组件来形容React机制,与之前的文档天壤之别,这就导致了很多老旧我的项目的Hooks代码是过期的,并不合乎其新文档的理念,这里将对Hooks最佳实际进行总结。

为什么须要Hooks

Hooks公布之初React就在其老版官网文档介绍了动机,次要有如下几个起因。

  • 很难在组件之间复用有状态的逻辑

    对于之前的类组件,状态都是与组件绑定到一起的,能够对待成组件的公有属性。这样的话,波及到有状态逻辑在组件间重用就变得很艰难,类组件的解决办法为render props或者高阶组件。然而当你用到这两种模式的时候,你须要扭转你的组件构造,并且会带来很多不必要的组件嵌套,让调试变得艰难,代码的复杂程度也会晋升很多。而Hooks将状态齐全与组件抽来到来,是一个独立的货色,这样就可能让咱们封装一般函数一样封装有状态逻辑,并且能轻松的在不同组件中复用。

  • 简单的类组件很难了解与保护

    因为单向数据流与状态晋升的起因,咱们很容易遇到须要同时解决很多逻辑的巨型组件,这带来了很重大的耦合性,一个componentDidMount生命周期里可能会蕴含很多不相干的代码,并且一个1000行的组件保护起来也足够令人头疼。

  • 类的学习老本很高

    React组件分为两种,类组件与函数式组件,这两者的复杂度相差微小,类组件带来了相当多的模板代码与难以了解的this指向。在Hooks诞生之前,函数式组件是无状态的,应用场景十分受限,然而当有Hooks之后,咱们所有的组件都能够应用更简洁更容易了解的函数式组件。

老版文档对于Hooks应用引起的误导

对于Hooks产生动机方面,老版文档解释的十分分明,然而对于Hooks如何应用,老版文档只具体说了useState,useEffect以及自定义Hooks,其中useEffect的应用很具备误导性,至于其余的Hooks只列了一个api列表简略的形容了一下各自的作用。

useState的应用根本没什么争议,咱们次要看useEffect

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>  );}

留神其官网示例中对于useEffect的正文,相当于让开发者认为它是生命周期的平替,这在最新的React文档中恰好是谬误的做法。当然,可能思考到开发者刚开始从类组件转换到Hooks函数式组件须要一个参照或者一个最简略的上手示例,这样是最明了的表达方式。不过现如今新文档曾经公布,对于Hooks的解释十分清晰且深刻,这次咱们就专一于新文档重塑对于Hooks的认知。

最佳实际

  • useState

    咱们都晓得React是基于函数式回调来更新UI的,状态的扭转不能间接赋值,须要调用特定的setState函数来告诉React更新DOM,类组件中通过从新执行render函数来实现,函数式组件因为没有render函数,所以其整个函数体就会在更新DOM的时候从新调用,这叫做re-render,那么函数中的一般变量就无奈表演状态的角色,因为每次函数从新调用,都会生成一个新的变量,导致每次都是初始值

    function Component() {  let count = 0;  // 每次re-render,函数从新执行,count始终是0    return (    <div>{ count }</div>  );}

    如果咱们须要在每次re-render都记住一个状态的最新值,那就须要应用useState hook,其始终会返回状态的最新值。

    import { useState } from 'react';function Component() {  const [count, setCount] = useState(0); // 每次re-render,count返回setCount设置的最新值    function handleIncrease() {    setCount(count + 1); // 触发re-render  }    return (    <div>      <button onClick="handleIncrease">+</button>      { count }    </div>  );}

    须要留神的是,useState返回的set函数并不会立即更新状态值,而是会批量更新,所以在set函数执行后,状态可能还是原始值,须要等到下次render值才会更新,所以如果屡次调用set函数并且依赖了状态值,后果可能在意料之外。

    import { useState } from 'react';function Component() {  const [count, setCount] = useState(0);    function handleIncrease3() {    // 并不会+3,每次点击还是只+1    setCount(count + 1); // setCount(0 + 1)    setCount(count + 1); // setCount(0 + 1)    setCount(count + 1); // setCount(0 + 1)  }    return (    <div>      <button onClick="handleIncrease3">+</button>      { count }    </div>  );}

    如果能够预料到一次操作须要屡次更新并且依赖上一次更新的值,那么set函数应该传入函数来更新值。

    import { useState } from 'react';function Component() {  const [count, setCount] = useState(0);    function handleIncrease3() {    // 这次与预期相符    setCount(pre => pre + 1); // setCount(0 + 1)    setCount(pre => pre + 1); // setCount(1 + 1)    setCount(pre => pre + 1); // setCount(2 + 1)  }    return (    <div>      <button onClick="handleIncrease3">+</button>      { count }    </div>  );}

    这种状况并不常见,通常在用户交互的事件处理函数中,比方click,React会在下一次事件触发之前更新状态,所以如果在一次事件处理函数中没有屡次更新的状况下,setCount(count + 1)任然是牢靠的。

    对于援用类型的状态,React举荐开发者听从不可变准则,即不要扭转现存的援用类型状态的外部值,具体起因这里有具体阐明,在此不赘述,提供一些例子供参考。

    import { useState } from 'react';function Component() {  const [userInfo, setUserInfo] = useState({    user: {      name: '',      age: 0    },    permissions: []  });    function handleChangeName(name) {    setUserInfo({      ...userInfo,      user: {        ...userInfo.user,        name      }    });  }    function handleAddPermission(permission) {    setUserInfo({      ...userInfo,      permissions: [        ...userInfo.permissions,        permission      ]    });  }    return (    <div>      <button onClick="handleChangeName">Submit User Name</button>      <button onClick="handleAddPermission">Add Permission</button>    </div>  );}
  • useRef

    因为每次re-render函数组件的函数体都会从新执行,所以定义的一般变量每次都会变成初始值,而useState生成的值必须通过set函数更新而触发re-render,如果咱们仅须要缓存某个变量值而不心愿扭转它的时候造成从新渲染(因为这个变量与UI无关),那么就须要应用useRef钩子函数。

    import { useRef } from 'react';function Component() {  const intervalRef = useRef(0);    function handleIncrease() {    intervalRef.current = setInterval(() => {      // ...    }, 1000);  }    return (    <div>      <button onClick="handleIncrease">+</button>      { count }    </div>  );}

    useRef返回的值是一个对象,蕴含一个current属性,这个属性的值才是ref的实在值,所以无论是读取还是设置值,都须要.current。另外须要留神的是,不要在render期间读取或者扭转ref的值,这尽管不会像useState一样造成死循环,然而这违反了React函数组件必须是纯函数的准则。

    useRef另一个常见的用例是操作DOM,当咱们须要援用DOM元素时,能够应用ref属性配合useRef来实现

    import { useRef } from 'react';function Component() {  const inputRef = useRef(null);    function handleFocus() {    inputRef.current.focus();  }    return (    <div>      <input ref={inputRef} />      <button onClick="handleFocus">Focus</button>    </div>  );}

    useRef除了能够援用原生DOM元素外,还能够援用React组件,这须要配合另一个钩子函数useImperativeHandleforwardRef来实现,后文再介绍其用法。

  • useEffect

    接下来就是须要重点关注的useEffect,咱们先来看React文档对于useEffect的定义:

    useEffect is a React Hook that lets you synchronize a component with an external system.

    这里提到的关键词是内部零碎,查阅其深层的阐明,会发现文档对于useEffect的定位是一个escape hatch,除非应用经典的React范式无奈解决的场景,否则不举荐应用他,移除不必要的Effect会让你的代码变的更易读,更快,更不容易出错。接下来咱们会着重探讨哪些场景须要应用useEffect,而哪些场景中的useEffect又是不必要的,当然在这之前,先介绍一下useEffect的用法。

    import { useState, useEffect } from 'react';function Component() {  const [count, setCount] = useState(0);    // 第一个参数是一个回调函数,会依据第二个参数在每次render之后执行  // 第二个参数是依赖数组,依赖蕴含props, state, 以及函数组件内的任何变量(不蕴含ref)  // 依赖数组如果不传,那么每次render之后都会执行回调  // 如果传空数组,那么只执行一次  // 如果传对应的响应变量,那么回调只会在响应变量变动的时候执行  // 响应变量是否变动应用Object.is()比拟  useEffect(() => {    console.log('effect run');  // 这里每次render都会执行        return () => {};  // 革除函数,若存在,则每次先执行革除函数再执行下一次Effect  });    useEffect(() => {    console.log('effect run');  // 这里只会在第一次render时执行  }, []);    useEffect(() => {    console.log('effect run', count);  // 第一次render立马执行,而后仅在count变动的时候执行  }, [count]);    function handleIncrease() {    setCount(count + 1);  }    return (    <div>      <button onClick="handleIncrease">+</button>      <span>{ count }</span>    </div>  );}

    useEffect的应用场景大略分为以下几个类别:

    • 与内部零碎交互

      import { useEffect } from 'react';import { createConnection } from './chat.js';function ChatRoom({ roomId }) {  const [serverUrl, setServerUrl] = useState('https://localhost:1234');  useEffect(() => {      const connection = createConnection(serverUrl, roomId);    connection.connect();      return () => {      connection.disconnect();      };  }, [serverUrl, roomId]);  // ...}

      比方这个React文档的官网示例,页面须要在初始化的时候就连贯一个聊天室,聊天室齐全是属于一个内部的零碎,利用内不关怀他是如何实现的,聊天室只对外裸露了connectdisconnect两个办法,这种时候就只能应用useEffect实现。

    • 管制非React组件

      利用开发时,咱们常常会遇到一些第三方组件,其实现形式并不是React,咱们无奈通过props的形式应用它,那么这个时候也只能应用useEffect

      import { useRef, useEffect } from 'react';import { MapWidget } from './map-widget.js';export default function Map({ zoomLevel }) {  const containerRef = useRef(null);  const mapRef = useRef(null);  useEffect(() => {    if (mapRef.current === null) {      mapRef.current = new MapWidget(containerRef.current);    }    const map = mapRef.current;    map.setZoom(zoomLevel);  }, [zoomLevel]);  return (    <div      style={{ width: 200, height: 200 }}      ref={containerRef}    />  );}

      比方这个地图组件,它的缩放倍数是通过react状态管制的,当倍数变动的时候,就须要通过useEffect调用地图组件的API去同步这个状态,这个地图组件也能够称为内部零碎。

    • 申请数据

      这个应该是useEffect最常见的应用场景了,因为页面上绝大多数信息都是须要申请接口获取,并且这个机会是在页面初始化的时候,这个时候也只能应用useEffect。然而像提交表单这样的事件驱动的申请是不须要useEffect的,只须要在对应的事件里发送申请就好了。

      import { useState, useEffect } from 'react';import { fetchData } from './api.js';export default function Page({ id }) {  const [list, setList] = useState([]);  useEffect(() => {    let ignore = false;  // 这里是为了避免race condition导致bug    fetchData(id).then(result => {      if (!ignore) {        setList(result);      }    });    return () => {      ignore = true;    };  }, [id]);  // ...}

    以上场景都是正确应用useEffect的状况,并且个别状况下都举荐将Effect封装成自定义Hook,以进步代码的可读性与维护性。

    import { useState, useEffect } from 'react';import { fetchData } from './api.js';function useList(id) {  const [list, setList] = useState([]);    useEffect(() => {    let ignore = false;    fetchData(id).then(result => {      if (!ignore) {        setList(result);      }    });    return () => {      ignore = true;    };  }, [id]);    return list;}export default function Page({id}) {  const list = useList(id);  // 自定义Hook}

    应用useEffect的时候,有一个特地重要的点是,确保第二个参数,即依赖数组的正确性。咱们我的项目中的代码很可能会有这种状况存在:

    useEffect(() => {  // ...  //  Avoid suppressing the linter like this:  // eslint-ignore-next-line react-hooks/exhaustive-deps}, []);

    应用正文让linter疏忽这个校验,不到万不得已尽量不要这么做,bug很可能就是从这里产生。那为什么咱们我的项目中还是会有这种状况呢,因为很有可能这些依赖项不必要的呈现在了Effect函数体中,开发者意识到了这并不是一个依赖项,偷懒式的应用正文一句话解决。真正要解决这个问题,须要扭转Effect函数体代码,将不必要的依赖变量移除,向React linter证实它并不是这个Effect的依赖,开发者须要思考,让这个Effect从新执行到底须要哪些依赖项。

    还有一个须要留神的点是,间接把函数组件内的一般对象变量与函数作为Effect的依赖可能会导致死循环,因为每次re-render,对象和函数都是另一个不同的援用,这会被Object.is认为不相等,如果有这样的依赖,思考应用useMemouseCallback,前面咱们会再说到这两个Hook。

    接下来说一下哪些场景没有必要应用useEffect:

    • props或者state变动更新另一个state

      import { useState, useEffect } from 'react';function Component({ firstName, lastName }) {  const [fullName, setFullName] = useState('');    useEffect(() => {    setFullName(firstName + ' ' + lastName);  }, [firstName, lastName]);}

      相似fullName这样的状态能够归类为计算属性,和Vue的计算属性概念是一样的,不过React中的计算属性能够间接在函数组件中应用一般变量定义,因为无论是props与state更新,函数都会从新执行,扭转量会始终是最新值。

      function Component({ firstName, lastName }) {  const fullName = firstName + ' ' + lastName;  // 应用计算属性代替useEffect}
    • props变动重置状态

      设想一个场景,有一个联系人信息页面,依据不一样的用户ID输出不一样的备注。

      import { useState, useEffect } from 'react';function ProfilePage({ userId }) {  const [comment, setComment] = useState('');  useEffect(() => {    setComment('');  // 用户ID变动重置备注  }, [userId]);    return (    <textarea value={comment} onChange={e => setComment(e.target.value)}/>  )}

      React默认应用同组件同地位的策略确定组件状态是否须要保留,为组件指定一个key能够让React依据key是否变动来代替默认策略。

      import { useState, useEffect } from 'react';function ProfilePage({ userId }) {  return (    <Profile      userId={userId}      key={userId}  // 通过key来重置state    />  );}function Profile({ userId }) {  const [comment, setComment] = useState('');    return (    <textarea value={comment} onChange={e => setComment(e.target.value)}/>  )}
    • 应用Effect解决用户交互事件

      import { useState, useEffect } from 'react';function Form() {  const [firstName, setFirstName] = useState('');  const [lastName, setLastName] = useState('');  const [jsonToSubmit, setJsonToSubmit] = useState(null);    //  Avoid: Event-specific logic inside an Effect  useEffect(() => {    if (jsonToSubmit !== null) {      post('/api/register', jsonToSubmit);    }  }, [jsonToSubmit]);  function handleSubmit(e) {    e.preventDefault();    setJsonToSubmit({ firstName, lastName });  }}

      这样的通过监听state的两头形式是不必要的,应该间接在事件处理函数中解决对应逻辑

      function Form() {  const [firstName, setFirstName] = useState('');  const [lastName, setLastName] = useState('');  function handleSubmit(e) {    e.preventDefault();    // ✅ Good: Event-specific logic is in the event handler    post('/api/register', { firstName, lastName });  }}

      总的来说,不必要应用useEffect能够分为两大类:

      1. render中的数据转换
      2. 用户交互事件触发的逻辑
  • useMemo、useCallback

    因为函数式组件re-render会将函数从新执行,如果咱们有一个重计算的计算属性,那么每次render会带来一些额定的性能开销,useMemo能够将数据缓存起来,除非依赖变动,才会从新计算结果。(useCallbackuseMemo针对函数数据类型提供的一个语法糖,和useMemo实质是一样的)

    import { useMemo } from 'react';import { someExpensiveCalc } from 'utils';function Component({ tree }) {  const filterTree = someExpensiveCalc(tree);  // 每次re-render都会从新执行重计算  const filterTreeCache = useMemo(    () => someExpensiveCalc(tree),    [tree]  );  // 只有tree变动的时候才会执行}

    useMemo不应该被滥用,没有必要为每一个计算属性都包裹上useMemo,这只是一个性能优化伎俩,如果这段计算代码并不消耗性能,包裹上useMemo没有任何益处,反而会升高代码可读性。如果你不确定某个计算是否消耗性能,能够在前后打印一下代码执行工夫,如果达到了1ms,那么应用useMemo是有好处的。

    useMemo的另一个用法是配合memo函数跳过组件re-render,React重渲染的逻辑是很激进的,一旦某个组件重渲染,会递归的让所有子组件都重渲染一遍,这就会带来一个问题,当很外层的父组件扭转一个状态,整个组件树全都重渲染了,当然一般来说这并不会造成性能问题,因为React在重渲染的过程中会做最小化更新,只更新扭转了的DOM。memo的应用动机是在开发者观测到了性能问题后,没有必要在刚开始的时候就应用memo进行性能优化。memo函数的用法是间接包裹组件,这样组件仅仅会在props变动的时候才进行re-render,然而咱们晓得,Object.is函数判断对象与函数,每一次都是一个不一样的值,所以如果你的组件的props是对象或者函数,并且须要应用memo进行性能优化的时候,须要用到useMemouseCallback

    import { memo, useMemo } from 'react';import { filterTodos } from 'utils';function List({ items }) {}const ListMemo = memo(List);function TodoList({ todos, tab, theme }) {  const visibleTodosCache = useMemo(    () => filterTodos(todos, tab),    [todos, tab]  );    return (    <div className={theme}>      <ListMemo items={visibleTodos} />    </div>  );}

    最初一个应用场景就是配合useEffect,或者useMemouseCallback自身进行依赖缓存,后面说到了,依赖项如果是一个对象或者函数,每次re-render都会是不一样的值,所以须要应用useMemo或者useCallback进行缓存。

    import { useMemo, useCallback, useEffect } from 'react';function Component({ text }) {  // const config = { type: 1, value: text };  const configCache = useMemo(() => ({ type: 1, value: text }), [text]);    // function getData() {}    const getDataCache = useCallback(() => {    // getData的逻辑    // ...  }, []);    useEffect(() => {    getDataCache(configCache);  }, [getDataCache, configCache]);}
  • useContext

    Context相似于Vue中的Provide与Inject,作用是透过组件层级传递属性,防止props多级透传的麻烦。

    import { createContext, useContext } from 'react';const ThemeContext = createContext('');function App() {  return (    <ThemeContext.Provider value="dark">      <Form />    </ThemeContext.Provider>  );}function Button() {  const theme = useContext(ThemeContext);}

    Context因为其便利性很容易被适度应用,然而也会带来数据流不明确的问题。如果仅仅是为了多级透传,并不一定要应用Context,能够尝试从新设计组件构造,应用children缩小props层级。或者罗唆就多穿几次props,尽管这样看起来很繁琐,然而会使你的利用数据流更清晰。这里总结了几个典型的Context实用场景:

    • 主题
    • 用户信息
    • 路由
    • 状态治理

      一个比拟经典的组合是Context + useReducer进行简单的状态治理,然而须要写不少的模板代码,相似与redux。

  • useImperativeHandle

    上文说到useRef的一个作用是援用React组件,这时候就须要用到useImperativeHandleforwardRef

    import { forwardRef, useImperativeHandle, useRef } from 'react';function App() {  const childRef = useRef(null);    return (    <div>      <Child ref={childRef} />      <button onClick={handleClick}>Focus</button>    </div>  );}const Child = forwardRef(function Child(props, ref) {  const inputRef = useRef(null);    useImperativeHandle(ref, () => {    return {      focus() {        inputRef.current.focus();      }    };  }, []);    return <input ref={inputRef} />});

    这种模式不应该被适度应用,从这个钩子函数的名字也能够看进去,除非是不得已的状况,尽量不要应用援用组件的模式,最好应用React props数据流的范式来构建利用。

  • 自定义Hooks

    除了上述内置钩子,开发者还能够封装本人的自定义钩子函数来复用有状态逻辑,这也是Hooks设计之初最重要的性能之一。然而自定义Hooks是一个特地大的主题,足以另开一篇文章细说,这次就简略说说自定义Hooks的根本准则。

    React对于Hooks的设计规定是,只能写在组件最顶层作用域,并且以use结尾的函数,具体的起因这里有阐明。自定义Hooks也应该遵循这种规定,留神不要用use结尾的函数去封装一些状态无关的工具函数,因为自定义Hooks肯定是状态相干的,换句话说就是,自定义Hook外部肯定用到了内置Hook或者其余自定义Hook,否则它就不应该以use结尾被称为Hook。

    自定义Hooks是专用有状态逻辑,而不是专用状态自身,每一个自定义Hook的状态都是独立的,并不会共用。

    封装自定义Hook的机会因人而异,然而你不须要对每一个细小的逻辑都做封装,自定义Hook应该封装为具体的下层业务逻辑,上面提供一些典型的封装场景:

    • useData(url)
    • useChatRoom(options)
    • useMediaQuery(query)

    同时防止封装上面这些形象的Hook:

    • useMount(fn)
    • useEffectOnce(fn)
    • useUpdateEffect(fn)