Подробные хуки React [почти 1W слов] + бой проекта

React.js

React Hooks

1. Что такое хуки

  • React всегда выступал за использованиефункциональный компонент, но иногда, когда вам нужно использовать состояние или какие-то другие функции, вы можете использовать толькокомпонент класса, потому что функциональные компоненты не имеют ни экземпляров, ни функций жизненного цикла, только компоненты класса имеют
  • Хуки — это новая функция в React 16.8, которая позволяет вам использовать состояние и другие функции React без написания классов.
  • Если вы пишете функциональный компонент и понимаете, что вам нужно добавить к нему некоторое состояние, раньше вам приходилось преобразовывать остальное в классы. Теперь вы можете использовать хуки непосредственно в существующих функциональных компонентах.
  • Все API React, начиная с использования, являются хуками.

2. Проблемы, решаемые хуками

1. Недостаточность компонентов класса

  • Логику состояния сложно использовать повторно:Логическое состояние трудно мультиплексировать между компонентами, это может занятьrender props(свойства рендеринга)илиHOC(компоненты более высокого порядка), но независимо от того, является ли это атрибутом рендеринга или компонентом более высокого порядка, он будет обертывать слой родительского контейнера (обычно элемент div) за пределы исходного компонента,приводит к иерархической избыточности
  • Имеет тенденцию быть сложным и трудным в обслуживании:
    • Смешивание ненужной логики в функциях жизненного цикла (например: вcomponentDidMountРегистрация событий и другой логики вcomponentWillUnmountВыгрузка событий посередине, такой разрозненный и несфокусированный метод написания, легко писать баги)
    • Компоненты класса полны доступа и обработки состояния, что затрудняет разделение компонентов на более мелкие компоненты.
  • это указывает на проблему: когда родительский компонент передает функцию дочернему компоненту, он должен связать это
    • Разница между четырьмя связывающими этими методами компонентами в реакции
class App extends React.Component<any, any> {
    handleClick2;

    constructor(props) {
        super(props);
        this.state = {
            num: 1,
            title: ' react study'
        };
        this.handleClick2 = this.handleClick1.bind(this);
    }

    handleClick1() {
        this.setState({
            num: this.state.num + 1,
        })
    }

    handleClick3 = () => {
        this.setState({
            num: this.state.num + 1,
        })
    };

    render() {
        return (<div>
            <h2>Ann, {this.state.num}</h2>
            <button onClick={this.handleClick2}>btn1</button>
            <button onClick={this.handleClick1.bind(this)}>btn2</button>
            <button onClick={() => this.handleClick1()}>btn3</button>
            <button onClick={this.handleClick3}>btn4</button>
        </div>)
    }
}

Предпосылка: оптимизация производительности была выполнена внутри подкомпонента, например (React.PureComponent)

  • Первыйпривязать это в конструкторе: Затем каждый раз, когда родительский компонент обновляется, если другие значения реквизита, переданные дочернему компоненту, остаются неизменными, дочерний компонент не будет обновляться;
  • ВторойВ функции render() для привязки внутри этого:потому чтоФункция привязки возвращает новую функцию, поэтому каждый раз, когда родительский компонент обновляется, функция будет перегенерирована, даже если другие значения реквизита, передаваемые родительским компонентом дочернему компоненту, остаются неизменными, дочерний компонент будет обновляться каждый раз;
  • ТретийИспользуйте стрелочные функции: когда родительский компонент обновляется, даже если тела функций двух стрелочных функций одинаковы, будет сгенерирована новая стрелочная функция, поэтому дочерний компонент будет обновляться каждый раз;
  • ЧетвертыйИспользуйте статические свойства класса: принцип аналогичен первому методу, и он более лаконичен, чем первый.

Подводя итог, если не обращать внимания, легко написать третий способ написания, что приведет к потере производительности.

2. Преимущество крючков

  • Три проблемы, которые могут оптимизировать компоненты класса
  • Возможность повторного использования логики состояния без изменения структуры компонента (пользовательские хуки)
  • Возможность разделения взаимосвязанных частей компонента на более мелкие функции (например, настройка подписок или запрос данных).
  • Разделение опасений по поводу побочных эффектов:Побочные эффекты относятся к логике, которая не возникает во время преобразования данных в представление, напримерajaxзапрос, доступ к родномуdomЭлементы, локальный постоянный кеш, события привязки/отвязки, добавление подписок, установка таймеров, логирование и т. д.. В прошлом эти побочные эффекты были записаны в функции жизненного цикла компонента класса. а такжеuseEffectОн будет выполнен после завершения рендеринга.useLayoutEffectв браузереlayoutПозже,paintingвыполнял раньше.

3. Вопросы, требующие внимания

  • только всамый внешний слой внутри функцииВызов хука, не вызывайте его в цикле, условном решении или подфункции
  • Доступно только в Reactфункциональный компонентХуки вызываются, а не в других функциях JavaScript
  • https://reactjs.org/warnings/invalid-hook-call-warning.html

В-четвертых, useState, useMemo и useCallback.

  • React предполагает, что когда вы вызываете useState несколько раз, вы можете гарантировать, что они будутпоследовательность вызововпостоянно.
  • Вызывая его в функциональном компоненте, чтобы добавить некоторое внутреннее состояние к компоненту, Reactсохранить это состояние при повторном рендеринге
  • Единственным параметром useState является начальное состояние.
  • useState вернет массив:состояние, функция, которая обновляет состояние
    • Во время начального рендеринга возвращаемое состояние (state) имеет то же значение, что и первый параметр (initialState), переданный в
    • Вы можете вызвать эту функцию в обработчике событий или в другом месте. Он похож на this.setState для компонентов класса, ноНовое состояние не будет объединено со старым состоянием, а будет заменено напрямую
// 这里可以任意命名,因为返回的是数组,数组解构
const [state, setState] = useState(initialState);

4.1 Пример использования

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

function Child1(porps) {
  console.log(porps);
  const { num, handleClick } = porps;
  return (
    <div
      onClick={() => {
        handleClick(num + 1);
      }}
    >
      child
    </div>
  );
}

function Child2(porps) {
  // console.log(porps);
  const { text, handleClick } = porps;
  return (
    <div>
      child2
      <Grandson text={text} handleClick={handleClick} />
    </div>
  );
}

function Grandson(porps) {
  console.log(porps);
  const { text, handleClick } = porps;
  return (
    <div
      onClick={() => {
        handleClick(text + 1);
      }}
    >
      grandson
    </div>
  );
}

function Parent() {
  let [num, setNum] = useState(0);
  let [text, setText] = useState(1);

  return (
    <div>
      <Child1 num={num} handleClick={setNum} />
      <Child2 text={text} handleClick={setText} />
    </div>
  );
}

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

4.2 Каждый рендер — это отдельное замыкание

  • Каждый рендер имеет свои реквизиты и состояние.
  • Каждый рендер имеет свой обработчик событий.
  • При нажатии на состояние обновления функциональный компонент будет вызываться снова, поэтому каждый рендеринг будет независимым, и последующие операции не повлияют на полученное значение.
function Counter2(){
  let [number,setNumber] = useState(0);
  function alertNumber(){
    setTimeout(()=>{
      // alert 只能获取到点击按钮时的那个状态
      alert(number);
    },3000);
  }
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
          <button onClick={alertNumber}>alertNumber</button>
      </>
  )
}

4.3 Функциональные обновления

  • Если новое состояние необходимо вычислить, используя предыдущее состояние, то функция обратного вызова может быть передана в качестве параметра setState. Функция обратного вызова получит предыдущее состояние и вернет обновленное значение.
function Counter(){
    let [number,setNumber] = useState(0);
    function lazy(){
        setTimeout(() => {
            // setNumber(number+1);
            // 这样每次执行时都会去获取一遍 state,而不是使用点击触发时的那个 state
            setNumber(number=>number+1);
        }, 3000);
    }
    return (
        <>
           <p>{number}</p>
           <button onClick={()=>setNumber(number+1)}>+</button>
           <button onClick={lazy}>lazy</button>
        </>
    )
}

4.4 Ленивая инициализация состояния

  • Параметр initialState будет работать только при начальном рендеринге компонента и будет игнорироваться при последующих рендерингах.
  • Если начальное состояние нужно получить сложным вычислением, вы можете передать функцию, вычислить и вернуть начальное состояние в функцию, эта функция вызывается только во время начального рендеринга.
function Counter5(props){
    console.log('Counter5 render');
    // 这个函数只在初始渲染时执行一次,后续更新状态重新渲染组件时,该函数就不会再被调用
    function getInitState(){
        return {number:props.number};
    }
    let [counter,setCounter] = useState(getInitState);
    return (
        <>
           <p>{counter.number}</p>
           <button onClick={()=>setCounter({number:counter.number+1})}>+</button>
           <button onClick={()=>setCounter(counter)}>setCounter</button>
        </>
    )
}

4.5 Оптимизация производительности

4.5.1 Object.is (поверхностное сравнение)

  • Хук внутренне использует Object.is для сравнения нового/старого состояния на равенство.
  • В отличие от метода setState в компоненте класса, если вы измените состояние, а переданное значение состояния не изменится, оно не будет перерисовано
  • В отличие от метода setState в компонентах класса, useState не объединяет объекты обновления автоматически. Вы можете использовать функциональный setState в сочетании с оператором распространения для объединения и обновления объектов.
function Counter(){
    const [counter,setCounter] = useState({name:'计数器',number:0});
    console.log('render Counter')
    // 如果你修改状态的时候,传的状态值没有变化,则不重新渲染
    return (
        <>
            <p>{counter.name}:{counter.number}</p>
            <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button>
            <button onClick={()=>setCounter(counter)}>++</button>
        </>
    )
}

4.5.2 Сокращение времени рендеринга

  • По умолчанию, пока состояние родительского компонента изменяется (независимо от того, зависит ли дочерний компонент от состояния или нет), дочерний компонент также будет перерисовываться.
  • Общая оптимизация:
    1. компонент класса:можно использоватьpureComponent;
    2. функциональный компонент:использоватьReact.memo, передавая функциональный компонент вmemoПосле этого он вернет новый компонент, функцию нового компонента:Если полученное свойство не изменилось, функция не перерисовывается.;
  • Но как сделать так, чтобы атрибуты не менялись? Здесь используется useState, каждое обновление независимо,const [number,setNumber] = useState(0) То есть каждый раз генерируется новое значение (даже если значение не изменяется), даже еслиReact.memo, он все равно будет перерисовываться
import React,{useState,memo,useMemo,useCallback} from 'react';

function SubCounter({onClick,data}){
    console.log('SubCounter render');
    return (
        <button onClick={onClick}>{data.number}</button>
    )
}
SubCounter = memo(SubCounter);
export  default  function Counter6(){
    console.log('Counter render');
    const [name,setName]= useState('计数器');
    const [number,setNumber] = useState(0);
    const data ={number};
    const addClick = ()=>{
        setNumber(number+1);
    };
    return (
        <>
            <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
            <SubCounter data={data} onClick={addClick}/>
        </>
    )
}
  • Более глубокая оптимизация:
    1. useCallback: получает встроенный параметр функции обратного вызова и массив зависимостей (дочерний компонент зависит от состояния родительского компонента, то есть дочерний компонент будет использовать значение родительского компонента), useCallback вернет запомненную версию обратного вызова функция, которая работает только в определенных обновлениях только при изменении зависимостей
    2. useMemo: передать функцию создания и массив зависимостей в качестве аргументовuseMemo, который пересчитывает запомненное значение только при изменении зависимости. Эта оптимизация помогает избежать дорогостоящих вычислений при каждом рендеринге.
import React,{useState,memo,useMemo,useCallback} from 'react';

function SubCounter({onClick,data}){
    console.log('SubCounter render');
    return (
        <button onClick={onClick}>{data.number}</button>
    )
}
SubCounter = memo(SubCounter);

let oldData,oldAddClick;
export  default  function Counter2(){
    console.log('Counter render');
    const [name,setName]= useState('计数器');
    const [number,setNumber] = useState(0);
    // 父组件更新时,这里的变量和函数每次都会重新创建,那么子组件接受到的属性每次都会认为是新的
    // 所以子组件也会随之更新,这时候可以用到 useMemo
    // 有没有后面的依赖项数组很重要,否则还是会重新渲染
    // 如果后面的依赖项数组没有值的话,即使父组件的 number 值改变了,子组件也不会去更新
    //const data = useMemo(()=>({number}),[]);
    const data = useMemo(()=>({number}),[number]);
    console.log('data===oldData ',data===oldData);
    oldData = data;
    
    // 有没有后面的依赖项数组很重要,否则还是会重新渲染
    const addClick = useCallback(()=>{
        setNumber(number+1);
    },[number]);
    console.log('addClick===oldAddClick ',addClick===oldAddClick);
    oldAddClick=addClick;
    return (
        <>
            <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
            <SubCounter data={data} onClick={addClick}/>
        </>
    )
}

4.6 Реализация связанного списка в исходном коде useState

import React from 'react';
import ReactDOM from 'react-dom';

let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;

function useState(initState) {
    let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};

    function setState(newState) {
        currentHook.memoizedState = newState;
        render();
    }
  	// 这就是为什么 useState 书写顺序很重要的原因
		// 假如某个 useState 没有执行,会导致指针移动出错,数据存取出错
    if (workInProgressHook.next) {
        // 这里只有组件刷新的时候,才会进入
        // 根据书写顺序来取对应的值
        // console.log(workInProgressHook);
        workInProgressHook = workInProgressHook.next;
    } else {
        // 只有在组件初始化加载时,才会进入
        // 根据书写顺序,存储对应的数据
        // 将 firstWorkInProgressHook 变成一个链表结构
        workInProgressHook.next = currentHook;
        // 将 workInProgressHook 指向 {memoizedState: initState, next: null}
        workInProgressHook = currentHook;
        // console.log(firstWorkInProgressHook);
    }
    return [currentHook.memoizedState, setState];
}

function Counter() {
    // 每次组件重新渲染的时候,这里的 useState 都会重新执行
    const [name, setName] = useState('计数器');
    const [number, setNumber] = useState(0);
    return (
        <>
            <p>{name}:{number}</p>
            <button onClick={() => setName('新计数器' + Date.now())}>新计数器</button>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}

function render() {
    // 每次重新渲染的时候,都将 workInProgressHook 指向 firstWorkInProgressHook
    workInProgressHook = firstWorkInProgressHook;
    ReactDOM.render(<Counter/>, document.getElementById('root'));
}

render();

Пять, используйте редьюсер

  • useReducer очень похож на редуктор в редукции.
  • useState реализуется внутри useReducer
  • Альтернатива useState, которая принимает редьюсер формы (состояние, действие) => newState и возвращает текущее состояние и его метод отправки.
  • В некоторых сценариях useReducer больше подходит, чем useState, например, логика состояния более сложная и содержит несколько подзначений, или следующее состояние зависит от предыдущего состояния и т. д.
let initialState = 0;
// 如果你希望初始状态是一个{number:0}
// 可以在第三个参数中传递一个这样的函数 ()=>({number:initialState})
// 这个函数是一个惰性初始化函数,可以用来进行复杂的计算,然后返回最终的 initialState
const [state, dispatch] = useReducer(reducer, initialState, init);
const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    )
}

Шесть, используйте контекст

  • Получает объект контекста (возвращаемое значение React.createContext) и возвращает текущее значение контекста.
  • Текущее значение контекста определяется значением свойства , ближайшего к текущему компоненту в верхнем компоненте.
  • Когда самый последний над компонентом обновляется, этот хук вызовет повторную визуализацию с последним значением контекста, переданным поставщику MyContext.
  • useContext(MyContext) эквивалентен компоненту класса в static contextType = MyContextили<MyContext.Consumer>
  • useContext(MyContext) просто позволяет вам читать значение контекста и подписываться на изменения в контексте. Вам по-прежнему необходимо использовать в верхнем дереве компонентов, чтобы предоставить контекст для нижних компонентов.
import React,{useState,memo,useMemo,useCallback,useReducer,createContext,useContext} from 'react';
import ReactDOM from 'react-dom';

const initialState = 0;
function reducer(state=initialState,action){
    switch(action.type){
        case 'ADD':
            return {number:state.number+1};
        default:
            break; 
    }
}

const CounterContext = createContext();
// 第一种获取 CounterContext 方法:不使用 hook
function SubCounter_one(){
    return (
        <CounterContext.Consumer>
            {
                value=>(
                    <>
                    <p>{value.state.number}</p>
                    <button onClick={()=>value.dispatch({type:'ADD'})}>+</button>
                    </>
                )
            }
         
        </CounterContext.Consumer>
    )
}
// 第二种获取 CounterContext 方法:使用 hook ,更简洁
function SubCounter(){
    const {state, dispatch} = useContext(CounterContext);
    return (
        <>
            <p>{state.number}</p>
            <button onClick={()=>dispatch({type:'ADD'})}>+</button>
        </>
    )
}
/* class SubCounter extends React.Component{
    static contextTypes = CounterContext
    this.context =  {state, dispatch}
} */

function Counter(){
    const [state, dispatch] = useReducer((reducer), initialState, ()=>({number:initialState}));
    return (
        <CounterContext.Provider value={{state, dispatch}}>
            <SubCounter/>
        </CounterContext.Provider>
    )
}
ReactDOM.render(<Counter  />, document.getElementById('root'));

Семь, используйтеЭффект

  • Эффект (побочный эффект): относится к логике, которая не возникает в процессе преобразования данных в представление, напримерajaxзапрос, доступ к родномуdomЭлементы, локальный постоянный кеш, события привязки/отвязки, добавление подписок, установка таймеров, логирование и т. д.
  • побочный эффект операцииможно разделить на дваДобрый:Что нужно чистить, а что не нужно чистить.
  • Изменение dom, отправка ajax-запросов и выполнение других операций с побочными эффектами внутри функциональных компонентов (в данном случае на этапе рендеринга React) не допускаются, поскольку это может привести к необъяснимым ошибкам и нарушению согласованности пользовательского интерфейса.
  • useEffect — это эффект-хук, который добавляет возможность манипулировать побочными эффектами в функциональные компоненты. Это то же самое, что и в компоненте классаcomponentDidMount,componentDidUpdateа такжеcomponentWillUnmountИмеет ту же цель, просто объединена в один API
  • useEffect принимает функцию, которая будет выполняться после рендеринга компонента на экран, у функции есть требования: либо возвращать функцию, очищающую побочный эффект, либо ничего не возвращать
  • а такжеcomponentDidMountилиcomponentDidUpdateНапротив, эффекты, отправленные с помощью useEffect, не блокируют обновление экрана браузером, что делает ваше приложение более отзывчивым. В большинстве случаев эффекты не должны выполняться синхронно. В отдельных случаях (например, при измерении макетов) существует отдельный хук useLayoutEffect, который можно использовать с тем же API, что и useEffect .

7.1 Используйте компонент класса для реализации изменения заголовка

  • В этом классе нам нужно написать повторяющийся код в обеих функциях жизненного цикла, потому что во многих случаях мы хотим делать одно и то же, когда компонент загружается и обновляется. Мы хотим, чтобы он выполнялся после каждого рендеринга, но компоненты класса React не предоставляют такой метод. Даже если мы извлечем метод, нам все равно придется вызывать его в двух местах. а такжеuseEffect будет выполняться после первого рендера и после каждого обновления
class Counter extends React.Component{
    state = {number:0};
    add = ()=>{
        this.setState({number:this.state.number+1});
    };
    componentDidMount(){
        this.changeTitle();
    }
    componentDidUpdate(){
        this.changeTitle();
    }
    changeTitle = ()=>{
        document.title = `你已经点击了${this.state.number}次`;
    };
    render(){
        return (
            <>
              <p>{this.state.number}</p>
              <button onClick={this.add}>+</button>
            </>
        )
    }
}

7.2 Использование useEffect для изменения заголовка

  • При каждом повторном рендеринге генерируется новый эффект, заменяющий предыдущий. В некотором смысле эффекты больше похожи на часть результата рендеринга — каждый эффект относится к определенному рендерингу.
import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
    const [number,setNumber] = useState(0);
    // useEffect里面的这个函数会在第一次渲染之后和更新完成后执行
    // 相当于 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        document.title = `你点击了${number}次`;
    });
    return (
        <>
            <p>{number}</p>
            <button onClick={()=>setNumber(number+1)}>+</button>
        </>
    )
}
ReactDOM.render(<Counter />, document.getElementById('root'));

7.3 Устранение побочных эффектов

  • Функция побочного эффекта также может указывать, как очистить побочный эффект, возвращая функцию.Чтобы предотвратить утечку памяти, функция очистки будет находиться вПеред удалением компонентоввоплощать в жизнь. Если компонент визуализируется несколько раз, предыдущий эффект очищается перед выполнением следующего эффекта.
function Counter(){
    let [number,setNumber] = useState(0);
    let [text,setText] = useState('');
    // 相当于componentDidMount 和 componentDidUpdate
    useEffect(()=>{
        console.log('开启一个新的定时器')
        let $timer = setInterval(()=>{
            setNumber(number=>number+1);
        },1000);
        // useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
        // useEffect 在执行副作用函数之前,会先调用上一次返回的函数
        // 如果要清除副作用,要么返回一个清除副作用的函数
       /*  return ()=>{
            console.log('destroy effect');
            clearInterval($timer);
        } */
    });
    // },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
    return (
        <>
          <input value={text} onChange={(event)=>setText(event.target.value)}/>
          <p>{number}</p>
          <button>+</button>
        </>
    )
}

7.4 Эффекты пропуска для оптимизации производительности

  • Массив зависимостей управляет выполнением useEffect.
  • Если какое-то конкретное значение не меняется между повторными рендерингами, вы можете указать React пропустить вызов эффекта, передав массив в качестве второго необязательного параметра для useEffect.
  • Если вы хотите выполнить запуск эффекта только один раз (выполняется только при монтировании и размонтировании компонента), можно передать пустой массив ([]) в качестве второго параметра. Это говорит о том, что ваш эффект React не зависит от каких-либо свойств состояния или значения, поэтому его никогда не нужно будет повторять.
  • Рекомендуется включитьeslint-plugin-react-hooksсерединаexhaustive-depsправило. Это правило предупреждает и предлагает исправления при добавлении неправильных зависимостей.
function Counter(){
    let [number,setNumber] = useState(0);
    let [text,setText] = useState('');
    // 相当于componentDidMount 和 componentDidUpdate
    useEffect(()=>{
        console.log('useEffect');
        let $timer = setInterval(()=>{
            setNumber(number=>number+1);
        },1000);
    },[text]);// 数组表示 effect 依赖的变量,只有当这个变量发生改变之后才会重新执行 efffect 函数
    return (
        <>
          <input value={text} onChange={(event)=>setText(event.target.value)}/>
          <p>{number}</p>
          <button>+</button>
        </>
    )
}

7.5 Разделение проблем с использованием нескольких эффектов

  • Одной из целей использования Hook является решение проблемы, заключающейся в том, что функции жизненного цикла в классе часто содержат несвязанную логику, но связанная логика разделена на несколько разных методов.
// 类组件版
class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
  • настройки можно найтиdocument.titleКак логика делится наcomponentDidMountа такжеcomponentDidUpdate, как логика подписки делится наcomponentDidMountа такжеcomponentWillUnmountсередина. а такжеcomponentDidMountСодержит код для двух разных функций одновременно. Это может сделать функции жизненного цикла очень запутанными.

  • Хуки позволяют нам разделить код по назначению,а не функции жизненного цикла. React будет вызывать каждый эффект в компоненте по очереди, в порядке объявления эффектов.

// Hooks 版
function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

Восемь, используйтеLayoutEffect

  • useEffect не будет выполняться до тех пор, пока не будет завершен весь рендеринг.
  • useLayoutEffect будет выполняться после макета браузера и перед рисованием
  • Его сигнатура функции такая же, как и useEffect , но после всех изменений DOMСинхронизироватьэффект вызова
  • Вы можете использовать его для чтения макета DOM и синхронного запуска повторного рендеринга.
  • График обновления внутри useLayoutEffect будетСинхронизироватьобновить
  • По возможности используйте стандартный useEffect, чтобы избежать блокировки обновлений представления.
function LayoutEffect() {
    const [color, setColor] = useState('red');
    useLayoutEffect(() => {
        alert(color);
    });
    useEffect(() => {
        console.log('color', color);
    });
    return (
        <>
            <div id="myDiv" style={{ background: color }}>颜色</div>
            <button onClick={() => setColor('red')}>红</button>
            <button onClick={() => setColor('yellow')}>黄</button>
            <button onClick={() => setColor('blue')}>蓝</button>
        </>
    );
}

Девять, useRef и useImperativeHandle

8.1 useRef

  • Компоненты класса и элементы React используют React.createRef, а компоненты функций используют useRef.
  • useRef возвращает изменяемый объект ref, чейcurrentСвойство инициализируется параметром, переданным в (initialValue)
const refContainer = useRef(initialValue);
  • Объект ref, возвращаемый useRef, остается неизменным на протяжении всего жизненного цикла компонента, а это означает, что каждый раз, когда функциональный компонент повторно визуализируется, возвращаемый объект ref является одним и тем же (при использовании React.createRef ссылка будет воссоздаваться каждый раз). время повторного рендеринга компонента )
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
    let [number, setNumber] = useState(0);
    return (
        <>
            <Child />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
        </>
    )
}
let input;
function Child() {
    const inputRef = useRef();
    console.log('input===inputRef', input === inputRef);
    input = inputRef;
    function getFocus() {
        inputRef.current.focus();
    }
    return (
        <>
            <input type="text" ref={inputRef} />
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}
ReactDOM.render(<Parent />, document.getElementById('root'));

8.2 forwardRef

  • Поскольку функциональные компоненты не имеют экземпляров, функциональные компоненты не могут получать атрибуты ref, как компоненты класса.
function Parent() {
    return (
        <>
         // <Child ref={xxx} /> 这样是不行的
            <Child />
            <button>+</button>
        </>
    )
}
  • forwardRef может манипулировать объектом ref дочернего компонента в родительском компоненте.
  • forwardRef может пересылать объект ref в родительском компоненте элементу dom в дочернем компоненте.
  • Дочерние компоненты принимают props и ref в качестве параметров
function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child = React.forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  // 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
  // 只有当它被赋给某个元素的 ref 属性时,才会有值
  // 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
  // 那么父组件就可以操作子组件中的某个元素
  // 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
  // 所以就需要用到 forwardRef 进行转发
  const inputRef = useRef();//{current:''}
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

8.3 useImperativeHandle

  • useImperativeHandleЭто позволяет вам настраивать значение экземпляра, предоставляемое родительскому компоненту при использовании ref, и вы не можете позволить родительскому компоненту делать все, что он хочет.
  • В большинстве случаев следует избегать императивного кода, такого как ref. useImperativeHandle следует использовать с forwardRef
  • Родительский компонент может использовать несколько ссылок для управления дочерними компонентами.
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';

function Child(props,parentRef){
    // 子组件内部自己创建 ref 
    let focusRef = useRef();
    let inputRef = useRef();
    useImperativeHandle(parentRef,()=>(
      // 这个函数会返回一个对象
      // 该对象会作为父组件 current 属性的值
      // 通过这种方式,父组件可以使用操作子组件中的多个 ref
        return {
            focusRef,
            inputRef,
            name:'计数器',
            focus(){
                focusRef.current.focus();
            },
            changeText(text){
                inputRef.current.value = text;
            }
        }
    });
    return (
        <>
            <input ref={focusRef}/>
            <input ref={inputRef}/>
        </>
    )

}
Child = forwardRef(Child);
function Parent(){
  const parentRef = useRef();//{current:''}
  function getFocus(){
    parentRef.current.focus();
    // 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
    parentRef.current.addNumber(666);
    parentRef.current.changeText('<script>alert(1)</script>');
    console.log(parentRef.current.name);
  }
  return (
      <>
        <ForwardChild ref={parentRef}/>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

10. Пользовательский хук

  • Пользовательские хуки — это скорее условность, чем функция. Функция называется пользовательским хуком, если ее имя начинается с use и вызывает другие хуки.
  • Иногда мы хотим повторно использовать некоторую логику состояния между компонентами, либо с реквизитами рендеринга, либо с компонентами более высокого порядка, либо с избыточностью.
  • Пользовательские хуки позволяют добиться того же результата без добавления компонентов.
  • Хук — это способ повторного использования логики состояния, он не использует повторно само состояние.
  • На самом деле каждый вызов Hook имеет полностью независимое состояние.
import React, { useLayoutEffect, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function useNumber(){
  let [number,setNumber] = useState(0);
  useEffect(()=>{
    setInterval(()=>{
        setNumber(number=>number+1);
    },1000);
  },[]);
  return [number,setNumber];
}
// 每个组件调用同一个 hook,只是复用 hook 的状态逻辑,并不会共用一个状态
function Counter1(){
    let [number,setNumber] = useNumber();
    return (
        <div><button onClick={()=>{
            setNumber(number+1)
        }}>{number}</button></div>
    )
}
function Counter2(){
    let [number,setNumber] = useNumber();
    return (
        <div><button  onClick={()=>{
            setNumber(number+1)
        }}>{number}</button></div>
    )
}
ReactDOM.render(<><Counter1 /><Counter2 /></>, document.getElementById('root'));

11. Часто задаваемые вопросы

1. Используйтеeslint-plugin-react-hooksпроверять код на наличие ошибок и давать подсказки

{
  "plugins": ["react-hooks"],
  // ...
  "rules": {
    "react-hooks/rules-of-hooks": 'error',// 检查 Hook 的规则
    "react-hooks/exhaustive-deps": 'warn' // 检查 effect 的依赖
  }
}

2. Почему вы должны запускать Эффект каждый раз при обновлении

react.doc — это China.org/docs/hooks-…

3. Почему вы должны использовать хуки на верхнем уровне компонентов и использовать несколько хуков состояний или эффектов в одном компоненте, так как же React узнает, какое состояние соответствует какому useState?

  • React зависит от порядка вызова хуков, если вы можете гарантировать, что хуки вызываются в одном и том же порядке при каждом рендеринге. Тогда React можетuseStateа такжеuseEffectКорректность сохранения состояния хука между вызовами
function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}
// ------------
// 首次渲染
// ------------
useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm)     // 2. 替换保存 form 的 effect
useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle)     // 4. 替换更新标题的 effect
// ...

Пока порядок вызова хуков одинаков для всех рендеров, React может правильно связать внутреннее состояние с соответствующим хуком.. Но если мы поставим крючок (например,persistFormэффект) вызов внутри условного оператора?

// 🔴 在条件语句中使用 Hook 违反第一条规则
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

в первом рендереname !== ''Это условиеtrue, поэтому мы выполним этот хук. Но при следующем рендеринге мы можем очистить форму, и значение выражения станетfalse. Рендеринг в это время будет пропускать хук, а последовательность вызова хука изменилась:

useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 🔴 3 (之前为 4)。替换更新标题的 effect 失败

React не знает о второмuseStateЧто Крюк должен вернуть. React будет думать, что второй вызов хука в этом компоненте подобен последнему рендеру, который соответствуетpersistFormэффект, но не такой. Отсюда последующие вызовы ловушек выполняются раньше времени, что приводит к ошибкам.

Если мы хотим выполнить эффект условно, мы можем поместить предикат внутри хука:

useEffect(function persistForm() {
    // 👍 将条件判断放置在 effect 中
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

4. Пользовательские хуки должны начинаться сuseНачало?

Так и должно быть. Эта конвенция очень важна. Если вы не будете следовать ему, React не сможет автоматически проверить, нарушает ли ваш хук, потому что нет способа определить, содержит ли функция вызов своего внутреннего хука.Правила для хуков.

5. Будет ли использование одного и того же хука в двух компонентах иметь общее состояние?

Не будет. Пользовательские хуки — это механизм повторного использования логики состояния (например, установка для подписки и сохранения текущих значений), поэтому каждый раз, когда используется пользовательский хук, все состояния и побочные эффекты в нем полностью изолированы.

6. Несколько вызовов в компонентеuseStateилиuseEffect, каждый раз, когда вызывается хук, он получает независимое состояние, которое полностью независимо.

7. Когда компонент имеет несколько состояний, следует ли объединить несколько состояний в одно состояние или следует разделить состояние на несколько переменных состояния?

react.doc — это China.org/docs/hooks-…

  • Либо поместите все состояние в одно и то жеuseStateВ вызове либо каждое поле соответствуетuseStateВызов, эти два метода могут выполняться.
  • когда ты здесьдве крайностинайти баланс междуродственное состояние Объединение в несколько независимых переменных состояния, компонент будет более читабельным. Если логика состояния начинает усложняться, мы рекомендуем использоватьuseReducerуправлять этим или использовать пользовательский крюк.

8. Можно ли запустить эффект только при обновлении?

Это относительно редкий вариант использования. Если вам нужно, вы можетеиспользовать изменяемую ссылкуВручную сохраните логическое значение, указывающее, следует ли рендерить первый или последующие рендеры, а затем проверьте наличие этого флага в вашем эффекте. (Если вы обнаружите, что делаете это часто, вы можете создать для этого собственный хук.)

9. При вызове функции в useEffect объявите функцию в useEffect вместо внешнего объявления, а затем вызовите ее в useEffect.

react.doc — это China.org/docs/hooks-…

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }

  useEffect(() => {
    doSomething();
  }, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}

Трудно вспомнить, какие реквизиты и состояния используются функциями вне эффекта. Вот почемуЧасто вы захотите объявить нужные функции внутри эффекта.Это позволяет легко увидеть, от каких значений в области действия компонента зависит эффект:

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }

    doSomething();
  }, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)
}

ТолькоКогда функция (и функции, которые она вызывает) не ссылаются на пропсы, состояние и производные от них значения, вы можете смело исключить их из списка зависимостей. В следующем случае есть ошибка:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  async function fetchProduct() {
    const response = await fetch('http://myapi/product' + productId); // 使用了 productId prop
    const json = await response.json();
    setProduct(json);
  }
  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId`
  // ...
}

Рекомендуемое исправление — переместить эту функцию внутрь вашего эффекта.. Это позволяет легко увидеть, какие реквизиты и состояние использует ваш эффект, и убедиться, что все они объявлены:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  useEffect(() => {
    // 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
    async function fetchProduct() {
      const response = await fetch('http://myapi/product' + productId);
      const json = await response.json();
      setProduct(json);
    }
    fetchProduct();
  }, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
  // ...
}

10. Как элегантно получать данные в хуках

Woohoo. Робин спрашивает IE о допуске. /react-hooks…

code sandbox.IO/Yes/J VV KO Oh 8 temp…

import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });
  // 注意 async 的位置
  // 这种写法,虽然可以运行,但是会发出警告
  // 每个带有 async 修饰的函数都返回一个隐含的 promise
  // 但是 useEffect 函数有要求:要么返回清除副作用函数,要么就不返回任何内容
  useEffect(async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  }, []);
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
export default App;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });
  useEffect(() => {
    // 更优雅的方式
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, []);
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
export default App;

11. Не слишком полагайтесь на useMemo

  • useMemoТакже есть накладные расходы.useMemoОна будет "запоминать" некоторые значения. При этом при последующем рендере будет выниматься значение в зависимом массиве и сравниваться с последним записанным значением. Если оно не равно, функция обратного вызова будет выполняться заново В противном случае будет возвращено "запомненное" значение. Сам этот процесс будет потреблять определенное количество памяти и вычислительных ресурсов. Поэтому чрезмерное использованиеuseMemoМожет повлиять на работу программы.
  • В использованииuseMemoПеред этим следует рассмотреть три вопроса:
    • Перейти кuseMemoФункция дорого стоит?Некоторые вычисления являются дорогостоящими, и нам нужно «запоминать» возвращаемое значение, чтобы избежать пересчета при каждом рендеринге. Если вы выполняете недорогую операцию, вам не нужно запоминать возвращаемое значение. В противном случае используйтеuseMemoСтоимость сама по себе может перевешивать стоимость пересчета этого значения. Поэтому для некоторых простых операций JS нам не нужно использоватьuseMemoчтобы «запомнить» его возвращаемое значение.
    • Является ли возвращаемое значение исходным значением?Если рассчитанныйбазовый типзначение (string,boolean,null,undefined,number,symbol), то каждое сравнение равносильно и нижестоящий компонент не выполняет повторный рендеринг; если вычисленныйсложный типзначение (object,array), даже если значение не изменится, адрес изменится, что приведет к повторному рендерингу нижестоящего компонента. Так что нам тоже нужно «запомнить» это значение.
    • При написании пользовательского хука возвращаемое значение должно поддерживать согласованность ссылок.Потому что вы не можете быть уверены, как снаружи будет использоваться возвращаемое значение. Ошибки могут возникать, если возвращаемое значение используется как зависимость от других хуков, а ссылка несовместима каждый раз при повторном рендеринге (когда значения равны). Поэтому, если значение, представленное в пользовательском хуке, является объектом, массивом, функцией и т. д., его следует использовать.useMemo. чтобы гарантировать, что ссылка не изменится, когда значение будет тем же самым.

12. useEffect не может получить асинхронность в качестве функции обратного вызова.

Функция, полученная useEffect, либо возвращает функцию, очищающую побочный эффект, либо ничего не возвращает. А асинхронность возвращает обещание.

Woohoo. Робин спрашивает IE о допуске. /react-hooks…

image.png

12. Боевой проект

Реагировать на крючки

13. Ссылка

Официальный список вопросов

Реагировать правила использования

Вы действительно используете React Hooks, верно?

Много пользовательских крючковсклад

Понимание компонентов React и обоснования хуков из Preact

2019 и составлена ​​практический случай N поможет вам быстро мигрировать, чтобы реагировать крюки

14. Рекомендуемое чтение

Вы действительно понимаете жизненный цикл React?

Подробный React SSR [почти 1W слов] + 2 актуальных проекта

Реализация простого веб-пакета от 0 до 1

Непонимание переменных среды настройки Webpack