乐趣区

关于react.js:React-Hooks最佳实践

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}), );
      
      // 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)
退出移动版