Интерпретация документа React Hooks и краткое изложение

внешний интерфейс React.js

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

1. Введение

Хуки — это новая функция в React 16.8, которая позволяет вам писатькомпонент классаВы также можете использовать состояние и другие функции React.

Хуки обеспечивают мощный и выразительный способ повторного использования логики между компонентами.

import React, { useState } from 'react';



function Example() {

  // 声明一个新的叫做 count 的 state 变量

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



  return (

    <div>

      <p>You clicked {count} times</p>

      <button onClick={() => setCount(count + 1)}>Click me</button>

    </div>

  );

}

1.1 Мотивация

Хуки решают всевозможные, казалось бы, несвязанные проблемы, с которыми мы сталкивались за годы написания и поддержки тысяч компонентов.

1.1.1 Сложность повторного использования логики состояния между компонентами

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

Теперь вы можете использовать хуки для извлечения логики состояния из компонентов, чтобы эту логику можно было тестировать по отдельности и использовать повторно. Крюк делает тебяПовторное использование логики состояния без изменения структуры компонента.

1.1.2 Сложные компоненты становятся трудными для понимания

Каждый жизненный цикл часто содержит некоторую несвязанную логику. если мы можем бытьcomponentDidMountНастройте прослушиватели событий вcomponentWillUnmountОчищено. Связанный код, который необходимо изменить путем сравнения, разбивается, а совершенно несвязанный код смешивается в одном и том же методе. Это может легко привести к ошибкам и привести к несоответствиям в логике.

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

1.1.3 Непонятные классы

Использование классов должно использоваться безthis, а в JavaScriptthisНаправление непросто понять, в разных сценариях использования методаthisОбъекты, на которые указывают, разные.

1.2 Различия Компоненты класса и функции компонентов

Строго говоря,компонент классаа такжефункциональный компонентЕсть различия. Разные методы написания представляют разные методологии программирования. Класс — это инкапсуляция данных и логики.Если вы решите написать класс, вы должны написать связанные данные и операции в том же классе. В общем случае функции должны делать только одну вещь и возвращать значение. Если у вас есть несколько операций, каждая операция должна быть написана как отдельная функция. Кроме того, состояние данных должно быть отделено от метода действия. Согласно этой идее,React Функциональный компонент должен делать только одно: возвращать HTML-код компонента..

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

Использование хуков делает код более лаконичным, а функциональные компоненты больше соответствуют функциональной природе React.

Hook Hook — это решение для побочных эффектов функциональных компонентов React, которое используется для введения побочных эффектов в функциональные компоненты. Тело функционального компонента должно использоваться только для возврата HTML-кода компонента, все остальные операции (побочные эффекты) должны быть импортированы через хуки.

Так как есть так много побочных эффектов, есть много видов крючков. React предоставляет специальные хуки для многих распространенных операций.

  • useStateсохранить состояние
  • useContextконтекст доступа
  • useRefсохранить ссылку
  • ...
  • useEffectОбщий хук побочного эффекта, он используется, чтобы узнать, не можете ли вы найти соответствующий хук

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

  • выборка данных
  • Мониторинг событий или подписка (настройка подписки)
  • изменение DOM (изменение DOM)
  • выходной журнал (журналирование)

Многие идеи React повлияли на всю отрасль, например, виртуальный DOM, JSX, функциональное программирование, неизменяемое состояние, односторонний поток данных и т. д. Хуки также привнесут важные инновации во внешний интерфейс.

1.3 Инкрементная стратегия

Команда React готова делать хуки повсюдукомпонент классасценариев использования, но по-прежнему будеткомпонент классапредоставить поддержку. Хуки и существующий код могут работать одновременно, и вы можете использовать их постепенно.

2 useStateсохранить состояние

useStateаналогичны компонентам классаthis.setState.

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42) // 原始值类型
  const [todos, setTodos] = useState([{text: 'Learn Hooks'}]) // 对象
  const [fruit, setFruit] = useState(() => 'banana') // 函数
  // ...
}

useState

  • Единственным параметром является начальное состояние, переданное значение будет использоваться только при первой инициализации.
  • Возвращаемое значение: текущее состояние и функция, которая обновляет состояние.
  • Во время первоначального рендеринга возвращаемое состояние состояния совпадает со значением, переданным в качестве первого параметра initialState.
  • Функция setState используется для обновления состояния. Он получает новое значение состояния и ставит в очередь повторный рендеринг компонента.
  • Вообще говоря, переменные «исчезают» после выхода из функции, за исключением переменных в состоянии, которые React сохраняет.
  • React гарантирует, что идентификатор функции setState стабилен и не изменится при повторном рендеринге компонента.

useStateа такжеthis.setStateразница между

  • useStateНе будет объединять новое состояние со старым состоянием
  • ДатьuseStateПередаваемое состояние может быть любого типа, иthis.setStateможет быть только объектом

2.1 Функциональное обновление

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

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

setState(prevState => {
  return {...prevState, ...updatedValues};
});

2.2 Ленивое начальное состояние

Параметр initialState будет работать только при начальном рендеринге компонента и будет игнорироваться при последующих рендерингах. Если начальное состояние нужно получить путем сложных вычислений, томожет передаваться в функциюИ возвращается в исходное состояние рассчитывается в функции, функция вызывается только при исходном рендеринге:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props); // 只在组件初始化时执行一次
  return initialState;
});

// 错误的用法,someExpensiveComputation 在每次组件重新渲染时都会执行
const [state, setState] = useState(someExpensiveComputation(props));

2.3 Пропустить обновление состояния

При вызове функции обновления State Hook и передаче текущего состояния React пропустит рендеринг дочерних компонентов и выполнение эффектов.

React может по-прежнему нуждаться в рендеринге компонента, прежде чем пропускать рендеринг. Но поскольку React не делает узлы излишне «глубже» в дереве компонентов, вам не нужно об этом беспокоиться. Если вы выполняете дорогостоящие вычисления во время рендеринга, вы можете использоватьuseMemoоптимизировать.

2.4 Руководство по избеганию ям

useStateВозвращаемый метод состояния обновления является асинхронным, и новое значение нельзя получить до следующей перерисовки.

const [count, setCount] = useState(0);
setCount(1);
console.log(count);  // 是 0 不是 1

Использование обратных вызовов может помочь нам получить последнее состояние. В примере нижеcurrent => current + 1Хотя написание более прямоеcount + 1Немного больше ввода, но результат больше соответствует нашим интуитивным ожиданиям.

const App3 = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(current => {
      console.log(1, {count, current}); // 1 {count: 0, current: 0}
      return current + 1;
    });

    setCount(current => {
      console.log(2, {count, current}); // 2 {count: 0, current: 1}
      return current + 1;
    });
  };



  useEffect(() => {
    console.log(3, {count}); // 3 {count: 2}
  }, [count]);

  return <div onClick={handleClick}>{count}</div>;

}

2.5 Поведенческое тестирование

  • setState обновляет состояние компонента, компонент функции будет запущен2 раза реагировать JS.org/docs/strict…
  • setState, передающий тот же ссылочный тип или значение примитивного типа, не будет запускать обновление компонента
  • setArray([...array])Хотя содержимое, соответствующее элементу ArrayItem, не изменилось, оно все равно вызовет повторную визуализацию.

2.5.1 React.FunctionComponent vs React.PureComponent

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

  • FunctionComponent всегда выполняется
  • PureComponentrenderМетод не будет выполняться, но система предусмотрена по умолчаниюshouldComponentUpdateОн все равно будет выполнен [Примечание 1]

Для сравнения, производительность PureComponent лучше.В большинстве случаев эта небольшая разница в производительности не учитывается, но ее можно использовать для оптимизации в ключевых случаях. Конечно, используяReact.memo()БудуFunctionComponentТакой же оптимизации можно добиться, обернув один слой.

Примечание 1: ФактическийPureComponentне существует и не может существовать вshouldComponentUpdate, фактический код выглядит следующим образом. Сравните один в ReactClassComponentНужно ли его обновлять, там всего два места. Сначала посмотрите, есть лиshouldComponentUpdateметод, второй здесьPureComponentсудить.

if (ctor.prototype && ctor.prototype.isPureReactComponent) {
  return (
    !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
  );
}

3 useEffectдобавить побочные эффекты

Возможно, вы уже выполняли компоненты React раньше.Данные получены, подписаны или изменены вручную DOM. В совокупности мы называем эти операции «побочными эффектами» или просто «действиями».

useEffectЭто хук эффектов, который добавляет возможность управлять побочными эффектами функциональных компонентов. Это то же самое, что и в компоненте классаcomponentDidMount,componentDidUpdateа такжеcomponentWillUnmountИмеет ту же цель, просто объединен в один API 👏👏.

import React, { useState, useEffect } from 'react'

function Example() {
  const [count, setCount] = useState(0)
  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={() => setCount(c => c + 1)}>Click me</button>  // setCount 支持回调函数
    </div>
  )
}

когда ты звонишьuseEffect, вы говорите React запустить вашу функцию «побочного эффекта» после внесения изменений в DOM. В компоненте класса функция рендеринга не должна иметь побочных эффектов. В общем, выполнять операции здесь рано, поэтому обычно мы помещаем побочные операции в компонентах класса вcomponentDidMountа такжеcomponentDidUpdateсередина.

Поскольку функции побочных эффектов объявлены внутри компонента, они могут получить доступ кpropsа такжеstate.

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

Побочные функции также могут бытьвернуть функциючтобы указать, как «очистить» побочные эффекты.

несмотря на то что useEffect Задержит выполнение после того, как браузер отрисует, но гарантированно выполнится до любых новых рендеров.. React обновит эффект предыдущего рендеринга перед обновлением компонента.

Почему он вызывается внутри компонента useEffect ?

БудуuseEffectРазмещение его внутри компонента дает нам прямой доступ к свойствам или переменным состояния в эффектах. Нам не нужен специальный API для его чтения, он уже хранится в области видимости функции.Хук использует механизм закрытия JavaScript.

useEffect Будет ли он выполняться после каждого рендера?

Да, по умолчанию он выполняется после первого рендера и после каждого обновления. Возможно, вам будет удобнее думать, что эффект возникает «после рендеринга», и вам не нужно беспокоиться о «монтировании» или «обновлении».

а такжеcomponentDidMountилиcomponentDidUpdateразные, используйтеuseEffectОтправленные эффекты не блокируют обновление экрана браузером, что делает ваше приложение более отзывчивым. В большинстве случаев эффекты не должны выполняться синхронно. В отдельных случаях (например, схемы измерения) существует отдельнаяuseLayoutEffectХук предназначен для использования вами, его API такой же, какuseEffectтакой же.

// 类组件里同一行代码要写两次

class Example extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`  // 1
  }

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

  render() { /* ... */ }
}
// 使用钩子一行搞定

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  })

  return ( /* ... */ )
}

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

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

использоватьuseEffectВажно отметить, что если есть несколько побочных эффектов, несколькоuseEffectа не объединять их вместе.

class FriendStatusWithCounter extends 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
    });
  }
  // ...
}
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);
    };
  });
  // ...
}

3.2 Эффекты, которые необходимо очистить

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

При каждом рендеринге React очищает предыдущий эффект перед выполнением текущего эффекта. Наконец, React выполнит еще одну очистку, когда компонент будет размонтирован.

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    // 添加副作用
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 通过返回一个函数来指定如何“清除”副作用:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

Примечание:useEffectФункция очистки естьПосле завершения нового рендеринга, но до повторного запуска функции нового побочного эффекта.бегать.

React запускает эффекты только после того, как позволяет браузеру рисовать. Это делает ваше приложение быстрее, поскольку большинству эффектов не нужно блокировать обновления экрана. Очистка эффекта также задерживается. Предыдущий эффект очищается после повторного рендеринга с новыми реквизитами.

function Foo() {
  const [count, setCount] = useState(0);
  if (count < 1) { setCount(count + 1); }
  console.log(`${count}-1`);
  useEffect(() => {
    console.log(`${count}-2`);
    return () => console.log(`${count}-3`);
  })
  return <div>Foo</div>;
}

// 初始化时输出
0-1
1-1
0-1
1-1
1-2
// 重新渲染时输出
1-1
1-1
1-3  // 此时浏览器已经重新渲染完成了,重新渲染完成后才会清理上一次的副作用
1-2

特别说明:以上是在 React.StrictMode + 开发环境 下的试验结果,非 StrictMode 下输出内容见下方
原因见 https://github.com/facebook/react/issues/15074#issuecomment-471197572

// 初始化时输出
0-1
1-1
1-2
// 重新渲染时输出
1-1
1-3
1-2

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

Опытные разработчики JavaScript могут заметить, чтоПерейти к useEffect Функция будет отличаться в каждом рендере, что сделано намеренно. На самом деле именно поэтому мы можем получить последнее значение счетчика в эффекте, не беспокоясь о его истечении. При каждом повторном рендеринге генерируется новый эффект, заменяющий предыдущий. В некотором смысле,эффект больше похож на часть результатов рендеринга- Каждый эффект "принадлежит" конкретному рендеру.

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

class FriendStatusWithCounter extends React.Component {

  // ...
  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  }
  // 如果没有这里的逻辑,那么当 friend 变化时,我们的组件展示的还是原来的好友状态
  componentDidUpdate(prevProps) {
    // 取消订阅之前的 friend.id
    ChatAPI.unsubscribeFromFriendStatus(prevProps.friend.id, this.handleStatusChange);
    // 订阅新的 friend.id
    ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
  }
  // ...
}

Вот как использовать хук

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

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

В некоторых случаях выполнение очистки или эффектов после каждого рендеринга может вызвать проблемы с производительностью. В компоненте класса мы можемcomponentDidUpdateДобавьте логику сравнения в prevProps или prevState для решения:

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

Это очень распространенное требование, поэтому оно встроено вuseEffectв Хук API. Если какое-то конкретное значение не меняется между повторными рендерингами, вы можете указать React пропустить вызов эффекта, передав массив какuseEffectВторой необязательный параметр может быть:

// 这个时候跟 Vue.js 的 watch 很像

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

注:经过试验,以下类型的值都有效

* 某个 state 值, 如 [count]
* state 的子属性, 如 [obj.count] // const [obj, setObj] = useState({count: 0})
* ref.current, 如 [ref.current.count] // const ref = useRef({})

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

Если вы передаете пустой массив [] , свойства и состояние внутри эффекта всегда будут иметь свои начальные значения.

Кроме того, помните, что React будет ждать, пока браузер закончит рендеринг экрана, прежде чем отложить вызов.useEffect, что делает дополнительные операции удобными.

3.5 Рекомендации по предотвращению ям

В разделе 3.4 написано, что «Если вы передаете пустой массив [] , свойства и состояние внутри эффекта всегда будут иметь свои начальные значения.", обнаруженный в реальном процессе кодирования, легко добавить по привычкеuseEffectВторой параметр , но часто забывают добавить зависимости, используемые внутри, в результате чего внутреннее значение функции не соответствует (в уме) ожиданиям. Конечно, эту проблему «React Hook необходимо вручную поддерживать зависимости» можно решить, настроив автоматическое исправление ESLint.

useEffect(() => {
  console.log(a); // 这里对 a 的引用是符合预期的
  console.log(b);// 这里对 b 的引用值会停留在上次 a 变更时的状态或(如果 a 没变更过)初始化时的状态 
}, [a]);  // 这里应该是 [a, b],但实际编码过程中很容易漏掉 b

4 useContextконтекст доступа

useContextполучает объект контекста (React.createContextвозвращаемое значение) и возвращает текущее значение контекста. Текущее значение контекста определяется ближайшим компонентом верхнего компонента к текущему компоненту.<MyContext.Provider value={xxx}>изvalueопорное решение.

Когда самый верхний компонент компонента является ближайшим<MyContext.Provider>При обновлении этот хук вызывает повторный рендеринг.
Если повторный рендеринг компонента обходится дорого, вы можете оптимизировать его с помощью мемоизации.
Если вы уже знакомы с контекстным API, прежде чем касаться Hook, должно быть понятно, что useContext(MyContext) эквивалентен компоненту класса вstatic contextType = MyContextили<MyContext.Consumer>.

Специальное примечание:

  • useContext(MyContext)Просто позволяет читать значение контекста и подписываться на изменения в контексте. Вам все еще нужно использовать в верхнем дереве компонентов<MyContext.Provider>для обеспечения контекста для базовых компонентов.
  • Будьте осторожны, чтобы не злоупотреблять контекстом, так как это нарушит независимость вашего компонента.
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },

  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}



function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}



function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

5 дополнительных крючков

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

5.1 useRef

  • Получите дескриптор дочернего компонента или узла DOM. Не удается получить ссылку на подкомпонент функции (но компонент функции может использоватьReact.forwardRefчтобы передать ref), он должен быть компонентом класса, поэтому класс нельзя полностью заменить на данный момент
  • Хранилище для общих данных между циклами рендеринга. состояние также может быть сохранено в циклах рендеринга, но вызовет повторный рендеринг, тогда как ref не вызовет повторный рендеринг.
  • Реф.currentЗначение Ref может быть изменено по желанию, но сам объект Ref не является расширяемым свойством.Object.isExtensible(ref) === false

useRef возвращает изменяемый объект ref, чейcurrentСвойство инициализируется переданным параметром initialValue. Возвращенный объект ref сохраняется на протяжении всего времени существования компонента.

Вы должны быть знакомы с ссылками, основным способом доступа к DOM. Если вы поместите объект ref с<div ref={myRef} />формы в компонент, независимо от того, как изменится узел, React передаст объект refcurrentСвойство устанавливается на соответствующий узел DOM.

Однако,useRef()СравниватьrefСвойства более полезны. Удобно хранить любое изменяемое значение, подобно тому, как поля экземпляра используются в классе.

Это потому, что он создает обычный объект Javascript. а такжеuseRef()и построить один{current: ...}Единственная разница в объектах заключается в том,useRefбудет возвращать один и тот же объект ref при каждом рендеринге.

Помните, что когда содержимое объекта ref изменяется,useRefи не уведомит вас.изменять current Свойство не вызывает повторную визуализацию компонента. Если вы хотите запустить некоторый код, когда React привязывает или отвязывает ссылку узла DOM, вам нужно использоватьref callbackреализовать.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

5.1.1 ref callback

React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  const refCallback = el => {
    console.log('refCallback', {el});
    inputEl.current = el;
  }

  return (
    <>
      <input ref={refCallback} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

5.2 useMemo

memo() Ограничивает повторную визуализацию компонента и useMemo() Это необходимо для ограничения многократного выполнения функции.
useMemo()а такжеuseEffect()Логика второго параметра такая же, за исключением того, чтоuseMemoимеет возвращаемое значение и выполняется перед рендерингом, иuseEffectвыполняется после рендеринга.
Передайте функцию «создать» и массив зависимостей в качестве аргументовuseMemo, который пересчитывает запомненное значение только при изменении зависимости. Эта оптимизация помогает избежать дорогостоящих вычислений при каждом рендеринге.
Помните, проходитеuseMemoфункция будет выполняться во время рендеринга. Пожалуйста, не выполняйте операции без рендеринга внутри этой функции, такие операции, как побочные эффекты, относятсяuseEffectсферы применения, а неuseMemo.
Если массив зависимостей не указан,useMemoНовое значение рассчитывается при каждом рендеринге.

// 这个跟 Vue.js 中的 computed 很像
const double = useMemo(() => count * 2, [count])

5.3 useCallback

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

useCallback(fn, deps)эквивалентноuseMemo(() => fn, deps).

// 套 memo 后,只要 props 没变就不会重新渲染
// `memo` 是一个 HOC,可以将 `Component` 或 `FunctionComponent` 转换成一个 `PureComponent`
// 本例中,App 内的 count 值变更,不会输出 “Foo render”,没套的话每次 App 的重新渲染都会触发 Foo 重复渲染
const Foo = memo(function Foo(props) {
  console.log('Foo render')
  // 这里必须显式绑定,在外层绑定不起作用,这个跟 Vue.js 行为不一样
  // 还可以写成 {...props} 这样通用性更强
  return <div onClick={props.onClick}>Me Foo</div>
})



const App = () => {
  const [count, setCount] = useState(0)
  // 没套 useCallback 的话,传递的函数句柄每次渲染都会变化,从而导致 Foo 重复渲染
  const clickFoo = useCallback(() => console.log('Foo Clicked'), [])
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Add</button> // 在 DOM 上无需 useCallback
      <Foo onClick={clickFoo} />                               // 传递给子组件就要套 useCallback
    </div>
  )
}

5.3.1 Рекомендации по предотвращению ям

useMemoа такжеuseCallbackВсе они представляют собой хуки, ориентированные на оптимизацию производительности, но новичкам легко ошибиться в их использовании: я не вижу, насколько лучше производительность, а код сложнее и нечитаемее (есть несколько слоев кода). код, который утомительно читать), и даже код отличается неожиданным поведением (с высокой вероятностью бытьuseMemo(xxx, dependencies)серединаdependenciesПропустил запись зависимостей, в результате чего не было получено последнее значение). Поэтому я лично рекомендую новичкам снова использовать эти два хука, когда они сталкиваются с проблемами производительности, и не использовать их без необходимости.

5.4 useReducer

В некоторых сценарияхuseReducerбыло бы лучше, чемuseStateболее применимо, напримерЛогика состояния сложна и содержит несколько подзначений., или следующее состояние зависит от предыдущего состояния. и, используяuseReducerВы также можете выполнять оптимизацию производительности для компонентов, запускающих глубокие обновления, поскольку вы можетеПередать диспетчеризацию дочерним компонентамвместо функции обратного вызова.

  • Redux: Global state management for user, auth, etc.

  • useReducer: Complex local state where dispatch is often passed to children as well.

  • useState: Simple local state where the setter is seldom passed to children.

I use all of the above.

  • Глобальное общее состояние для легкой отладки и обслуживания с использованием Redux
  • Простое использование состояния компонентаuseState
  • Сложное состояние компонента, которое требует нескольких типов операций или когда вам нужно передать сеттеры дочерним компонентам, используйтеuseReducer. Особенно, когда разные подкомпоненты должны выполнять разные операции над сложными состояниями, используйтеdispatchЭто может сделать операционное намерение дочернего компонента более ясным.
const initialState = {count: 0};
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

Есть два способа инициализировать состояние

  • Один - прямой вводuseReducer(reducer, initialState)
  • Одним из них является динамическое создание при инициализации (отложенная инициализация).useReducer(reducer, initialArg, init)

You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg).

It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action:

function init(initialCount) {
  return {count: initialCount};
}


function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}



function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

5.1.1 Рекомендации по предотвращению ям

useReducerтакже следоватьuseStateЕсть аналогичная проблема, сколько бы раз не звонилdispatch({type: 'a', payload: state.a + 1}), результаты те же. В настоящее время мы можем поддерживать использование обратного вызова путем преобразования редьюсера. Конкретную реализацию см. В примере ниже.

const reducer = (state, action) => {
  // 支持类似 dispatch(state => ({type: 'a', payload: state.a + 1})) 的写法
  if (typeof action === 'function') {
    action = action(state);
  }

  // 支持类似 dispatch({type: 'a', payload: state => state.a + 1})) 的写法
  if (typeof action.payload === 'function') {
    action.payload = action.payload(state);
  }

  console.log({state, action});

  if (action.type === 'a') {
    return { ...state, a: action.payload };
  } else {
    return state;
  }
}



const App = () => {
  const [state, dispatch] = useReducer(reducer, {a: 0})

  const handleClick = () => {
    dispatch({type: 'a', payload: state.a + 1});
    dispatch({type: 'a', payload: state.a + 1}); // 坑
    dispatch({type: 'a', payload: s => s.a + 1}); // 避坑
  };

  useEffect(() => {
    console.log('state in useEffect', state);
  })

  return <div onClick={handleClick}>Dispatch</div>;
}

5.5 useImperativeHandle

useImperativeHandleпозволяет вам использоватьrefПри настройке значения экземпляра, предоставляемого родительскому компоненту (обычное приложение пропускает func). Такого императивного кода следует избегать, насколько это возможно.useImperativeHandleнужно сforwardRefС использованием:

function FancyInput(props, ref) {
  const inputRef = useRef()
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus()
  }))
  return <input ref={inputRef} />;
}

FancyInput = forwardRef(FancyInput);

function Foo () {
  const fancyInputRef = useRef(null)
  return (
    <>
      <span onClick={() => fancyInputRef.current.focus()}></span>
      <FancyInput ref={fancyInputRef} />
    </>
  )
}

5.6 useLayoutEffect

It fires synchronously after all DOM mutations. We recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem.

Your code runs immediately after the DOM has been updated, but before the browser has had a chance to paint those changes (the user doesn't actually see the updates until after the browser has repainted).

Его сигнатура функции такая же, какuseEffectто же самое, но это будет ввсе DOM После изменения синхронизировать (т.е. блокировать)Вызов эффекта. Вы можете использовать его для чтения макета DOM и синхронного повторного рендеринга.до того, как браузер выполнит рисование,useLayoutEffectВнутреннее расписание обновлений будет обновляться синхронно.

useLayoutEffectа такжеcomponentDidMount,componentDidUpdateЭтап вызова тот же.

useLayoutEffectОн заблокирует основной поток браузера, и все изменения в нем будут отражены при следующем рендеринге. а такжеuseEffectОсновной поток будет остановлен первым, а задача будет добавлена ​​в очередь событий для выполнения. (Просто посмотрите на Task of DevTools/Performance/Main, просто увеличьте масштаб и посмотрите сразу)

Если вы используете рендеринг на стороне сервера...

5.7 useDebugValue

useDebugValueМетки, которые можно использовать для отображения пользовательских хуков в React DevTools.
В некоторых случаях отображение отформатированных значений может быть дорогостоящей операцией. В этом нет необходимости, если не требуется проверка хука.
следовательно,useDebugValueПринимает функцию форматирования в качестве необязательного второго аргумента. Эта функция будет вызываться только при проверке хука. Он принимает отладочное значение в качестве аргумента и возвращает отформатированное отображаемое значение.

6 пользовательских хуков

Пока что в React есть два популярных способа разделить логику состояния между компонентами: render props и компоненты более высокого порядка, теперь давайте посмотрим, как Hooks решают ту же проблему, не позволяя вам добавлять компоненты.

Пользовательский хук — это функция, имя которой начинается с «use» и может вызывать другие хуки внутри функции.

В соответствии с компонентами убедитесь, что другие хуки безоговорочно вызываются только поверх пользовательских хуков.

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

Пользовательский крючок - это естественное согласие дизайна крючка, а не React характеристики.

Вы можете создавать собственные хуки, которые охватывают множество сценариев, таких как обработка форм, анимация, объявления подписки, таймеры и, возможно, даже больше сценариев, которых мы не ожидали.

Должны ли пользовательские хуки начинаться с «использовать»?

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

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

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

import React, { useState, useEffect } from 'react';

// 自定义钩子

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

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }



  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}



// 组件

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}



// 组件

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

6.1 usePrevious

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

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

6.2 useForceUpdate

Используется для принудительного обновления компонента, обычно при использовании useRef для управления изменяемым состоянием, но его необходимо повторно отобразить.

function useForceUpdate() {
  const [, forceUpdate] = useReducer(v => v + 1, 0);
  return forceUpdate;
}



function Demo() {
  const counter = useRef(0);
  const forceUpdate = useForceUpdate();
  const handleClick = () => {
    counter.current++;
    forceUpdate();
  }
  return <div onClick={handleClick}>{counter.current}</div>;
}

6.3 Использование сторонних библиотек

Хуки, официально предоставляемые React, очень просты, и многие логики в реальном бизнесе можно использовать повторно. В реальном бою настоятельно рекомендуется использовать стороннюю библиотеку хуков для повышения эффективности. Внутренний проект Byte еще не является открытым исходным кодом, поэтому мы рекомендуем Али в первую очередь.ahooks.js.org/

7 правил крючка

React требует, чтобы использование хуков соответствовало двум правилам, которые ограничены текущей базовой реализацией хуков, и, возможно, в будущем таких правил не будет. Дело не в том, что я не хочу, а в том, что это временно не может быть реализовано.
Включая некоторые из подводных камней, упомянутых выше, все они связаны с текущим дизайном или реализацией Hook. Крючок уже очень хорош, но порога использования нет, или другими словами, до совершенства еще далеко.
Хуки — это просто функции JavaScript, но есть два дополнительных правила их использования.

7.1 Используйте хуки только на верхнем уровне

Не вызывайте хуки в циклах, условных выражениях или вложенных функциях, всегда вызывайте их на верхнем уровне вашей функции React. Следуя этому правилу, вы можете гарантировать, что хук будет выполняться при каждом рендеринге.вызываются в том же порядке. Это позволяет React поддерживать правильное состояние ловушки при нескольких вызовах useState и useEffect.

7.2 Перехватчики вызовов только в функциональных компонентах

Не вызывайте хуки в обычных функциях JavaScript. Ты сможешь:

  • Вызов хуков в функциональных компонентах React
  • Вызов других хуков в пользовательских хуках

Следование этому правилу гарантирует, что логика состояния компонента четко видна в коде.

7.3 Почему эти два правила

Обратитесь к часто задаваемым вопросам:How does React associate Hook calls with components?

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

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

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