Начало работы с React Hooks: основы

React.js
Начало работы с React Hooks: основы

предисловие

   Прежде всего, приглашаю всех подписаться на меняБлог на гитхабе, это можно расценивать как небольшое поощрение для меня.В конце концов, я не могу получить деньги, чтобы что-то написать.Я могу придерживаться своего собственного энтузиазма и всеобщего поощрения.Я надеюсь, что все обратят больше внимания! Функция Hooks была добавлена ​​в React 16.8, а модуль Hooks был добавлен в официальную документацию React, чтобы представить новые функции.Видно, что React придает большое значение Hooks.Если вы все еще не знаете, что такое Hooks, настоятельно рекомендуется их понять, ведь за этим может стать будущее React.   

источник

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

   Во-первых, для компонентов класса очень проблематично совместно использовать логику состояния. Например, мы заимствуем сцену из официального документа, компонент FriendStatus используется для отображения того, находится ли пользователь в списке друзей в сети.

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

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

   Вышеупомянутый компонент FriendStatus будет активно подписываться на статус пользователя при его создании и отписываться от статуса при его удалении, чтобы предотвратить утечку памяти. Предположим, есть еще один компонент, который также должен подписываться на онлайн-статус пользователя.Если мы хотим повторно использовать эту логику, мы обычно используемrender propsИ компоненты более высокого порядка для повторного использования логики состояния.

// 采用render props的方式复用状态逻辑
class OnlineStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

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

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    const {isOnline } = this.state;
    return this.props.children({isOnline})
  }
}

class FriendStatus extends React.Component{
  render(){
    return (
      <OnlineStatus friend={this.props.friend}>
        {
          ({isOnline}) => {
            if (isOnline === null) {
              return 'Loading...';
            }
            return isOnline ? 'Online' : 'Offline';
          }
        }
      </OnlineStatus>
    );
  }
}
// 采用高阶组件的方式复用状态逻辑
function withSubscription(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { isOnline: null };
      this.handleStatusChange = this.handleStatusChange.bind(this);
    }

    componentDidMount() {
      ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
      );
    }

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

    handleStatusChange(status) {
      this.setState({
        isOnline: status.isOnline
      });
    }

    render() {
      return <WrappedComponent isOnline={this.state.isOnline}/>
    }
  }
}

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

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

   Мало того, что по мере усложнения бизнес-логики в компоненте класса сложность обслуживания будет постепенно возрастать, поскольку логика состояния будет разделена на разные функции жизненного цикла, например, логика состояния подписки находится вcomponentDidMount, логика отписки находится вcomponentWillUnmountВ , код связанной логики отделен друг от друга, а код несвязанной логики может быть сконцентрирован вместе, и в целом это не способствует сопровождению. И по сравнению с функциональными компонентами, классовые компоненты более сложны в изучении, вам нужно все время быть начеку.thisЛовушки в компонентах, никогда не забывайте привязывать обработчики событийthis. При всем этом кажется, что функциональные компоненты по-прежнему обладают уникальными преимуществами.

Hooks

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

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

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

State: useState useReducer

  useStateЭто самый простой и часто используемый хук, предоставляемый React, который в основном используется для определения локального состояния.В качестве примера возьмем простой счетчик:

import React, { useState } from 'react'

function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <span>{count}</span>
      <button onClick={()=> setCount(count + 1)}>+</button>
      <button onClick={() => setCount((count) => count - 1)}>-</button>
    </div>
  );
}

  useStateМожет использоваться для определения состояния.В отличие от состояния, состояние может быть не только объектом, но и значением базового типа, например переменной типа Number выше.useStateВозвращается массив, первое из которых — фактическое значение текущего состояния, а второе — функция для изменения этого состояния, что-то вродеsetState. функция обновления сsetStateТо же самое, что оба могут принимать два типа параметров, значение и функцию, иuseStateРазница в том, что функция обновления заменяет состояние, а не объединяет его.

   Если в функциональном компоненте есть несколько состояний, вы можете передатьuseStateОбъявите состояние типа объекта, также черезuseStateУкажите несколько раз.

// 声明对象类型的状态
const [count, setCount] = useState({
    count1: 0,
    count2: 0
});

// 多次声明
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

   По сравнению с объявлением состояния типа объекта, очевидно, удобнее объявить состояние несколько раз, в основном потому, что функция обновления принимает метод замены, поэтому вам приходится добавлять неизменяемые атрибуты к параметрам, что очень хлопотно. Следует отметить, что React записывает каждое внутреннее состояние через последовательность вызовов Hook, поэтому Hook нельзя вызывать в условных операторах (таких как if) или операторах цикла, и следует отметить, что мы можем вызывать его только в функциональных компонентах Hooks, Хуки нельзя вызывать в компонентах и ​​обычных функциях (кроме пользовательских хуков).

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

import React, { useReducer } from 'react'

const reducer = function (state, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}

function Example() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  const {count} = state;
  return (
    <div>
      <span>{count}</span>
      <button onClick={()=> dispatch({ type: "increment"})}>+</button>
      <button onClick={() => dispatch({ type: "decrement"})}>-</button>
    </div>
  );
}

  useReducer принимает два параметра: функцию редуктора и значение по умолчанию, и возвращает массив текущего состояния состояния и функции отправки, и его логика в основном такая же, как у Redux. Разница между useReducer и Redux заключается в значении по умолчанию.Значение Redux по умолчанию задается путем назначения параметров по умолчанию функции редуктора, например:

// Redux的默认值逻辑
const reducer = function (state = { count: 0 }, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}

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

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, { count: initialState });
  // 省略...
}

   Таким образом, значение состояния по умолчанию может быть определено путем передачи реквизита.Конечно, хотя React не рекомендует метод значения по умолчанию Redux, он также позволяет назначать значения по умолчанию аналогично Redux. Это затронет третий параметр useReducer: инициализация.

Как следует из названия, третий параметр initialization используется для инициализации состояния. Когда useReducer инициализирует состояние, он передает второй параметр initialState в функцию инициализации. Значение, возвращаемое функцией initialState, является начальным состоянием состояния, которое позволяет абстрагироваться за пределы редуктора.Функция предназначена для вычисления начального состояния состояния. Например:

const initialization = (initialState) => ({ count: initialState })

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, initialState, initialization);
  // 省略...
}

   Итак, с помощью функции инициализации мы можем смоделировать начальное значение Redux:

import React, { useReducer } from 'react'

const reducer = function (state = {count: 0}, action) {
  // 省略...
}

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, undefined, reducer());
  // 省略...
}

Side Effects: useEffect useLayoutEffect

  解决了函数组件中内部状态的定义,接下来亟待解决的函数组件中生命周期函数的问题。在函数式思想的React中,生命周期函数是沟通函数式和命令式的桥梁,你可以在生命周期中执行相关的副作用(Side Effects),例如: 请求数据、操作DOM等。 React提供了useEffect来处理副作用。 Например:

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`
    return () => {
      console.log('clean up!')
    }
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

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

render -> effect callback -> re-render -> clean callback -> effect callback

   поэтому мы можем использоватьuseEffectИмитация поведения componentDidMount, componentDidUpdate, componentWillUnmount. Как мы упоминали ранее, именно из-за функции жизненного цикла мы должны разделить связанный код на разные функции жизненного цикла и вместо этого поместить нерелевантный код в ту же функцию жизненного цикла Причина, по которой это происходит, в основном проблема заключается в том, что мы пишем код не по бизнес-логике, а по времени выполнения. Чтобы решить эту проблему, мы можем решить вышеуказанную проблему, создав несколько хуков и поместив соответствующий логический код в один и тот же хук:

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

function Example() {
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  
  useEffect(() => {
    otherAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      otherAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // 省略...
}

   Мы используем несколько хуков, чтобы сосредоточить внимание на логических проблемах, чтобы избежать логической путаницы, вызванной сочетанием несвязанных кодов. Но тогда возникает проблема, если предположить, что одно из наших поведений направлено на то, чтобы различатьcomponentDidUpdateилиcomponentDidMountвыполняется, когдаuseEffectможно отличить. к счастьюuseEffectПредоставляет нам второй параметр, если второй параметр передается в массиве, только при изменении значения в массиве при повторном рендеринге,useEffectФункция обратного вызова будет выполнена. Поэтому, если мы передаем ему пустой массив, мы можем смоделировать жизненный циклcomponentDidMount. Но если вы хотите моделировать толькоcomponentDidUpdate, до сих пор не было найдено ни одного хорошего метода.

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

Контекст: использоватьконтекст

  С крючком:useContextМы также можем использовать функциональную сборкуcontext. По сравнению с использованием реквизитов рендеринга в компонентах класса,useContextвполне удобен в использовании.

import { createContext } from 'react'

const ThemeContext = createContext({ color: 'color', background: 'black'});

function Example() {
    const theme = useContext(Conext);
    return (
        <p style={{color: theme.color}}>Hello World!</p>
    );
}

class App extends Component {
  state = {
    color: "red",
    background: "black"
  };

  render() {
    return (
        <Context.Provider value={{ color: this.state.color, background: this.state.background}}>
          <Example/>
          <button onClick={() => this.setState({color: 'blue'})}>color</button>
          <button onClick={() => this.setState({background: 'blue'})}>backgroud</button>
        </Context.Provider>
    );
  }
}

  useContextпринять функциюReact.createContextВозвращенный объект контекста используется в качестве параметра для возврата значения в текущем контексте. в любое времяProviderПри изменении значения функция будет повторно отображать компонент, следует отметить, что даже при изменении значения неиспользуемого контекста функция будет повторно отображать компонент, как в примере выше,Exampleкомпонент, даже если он не используетсяbackground,ноbackgroundкогда происходят изменения,Exampleтакже будет повторно отображаться. Поэтому при необходимости, еслиExampleКомпоненты также имеют подкомпоненты, вам может потребоваться добавитьshouldComponentUpdateПредотвратите ненужный рендеринг из-за потери производительности.

Ref: useRef useImperativeHandle

  useRefРаспространенные примеры доступа к дочерним элементам:

function Example() {
    const inputEl = useRef();
    const onButtonClick = () => {
        inputEl.current.focus();
    };
    return (
        <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>Focus the input</button>
        </>
    );
}

   Мы сказали вышеuseRefобычно используется вrefсвойства, на самом делеuseRefделает больше, чем это

const refContainer = useRef(initialValue)

  useRefможет принимать значение по умолчанию и возвращатьcurrentИзменяемый объект свойств, который будет сохраняться в течение всего времени жизни компонента. Поэтому его можно использовать как свойство компонента класса.

  useImperativeHandleИспользуется для пользовательского доступа к родительским компонентамrefАтрибуты. нужно сотрудничатьforwardRefиспользовать вместе.

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

export default forwardRef(Example);
class App extends Component {
  constructor(props){
      super(props);
      this.inputRef = createRef()
  }
  
  render() {
    return (
        <>
            <Example ref={this.inputRef}/>
            <button onClick={() => {this.inputRef.current.focus()}}>Click</button>
        </>
    );
  }
}

New Feature: useCallback useMemo

   Студенты, знакомые с React, видели похожие сценарии:   

class Example extends React.PureComponent{
    render(){
        // ......
    }
}

class App extends Component{
    render(){
        return <Example onChange={() => this.setState()}/>
    }
}

   На самом деле в этом сценарии, хотяExampleнаследоватьPureComponent, но на самом деле не оптимизирует производительность, потому что каждый разAppкомпонент, переданный вonChangeсвойство является новым экземпляром функции, поэтому каждый разExampleбудет перерисован. Чтобы решить эту ситуацию, мы обычно используем следующие методы:

class App extends Component{
    constructor(props){
        super(props);
        this.onChange = this.onChange.bind(this);
    }

    render(){
        return <Example onChange={this.onChange}/>
    }
    
    onChange(){
        // ...
    }
}

  Описанный выше метод решает сразу две задачи: во-первых, он обеспечиваетExampleкомпонентonChangeСвойства — это тот же экземпляр функции, и для решения функции обратного вызоваthisСвязывание. Итак, как решить проблему функциональные компоненты, присутствующие в нем? РеагироватьuseCallbackФункция кэширования обработчиков событий.

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

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

а такжеuseCallbackПо аналогии,useMemoТо, что возвращается, является кэшированным значением.

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

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

useCallback(fn, input)ЭквивалентноuseMemo(() => fn, input)

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

Пользовательский крючок

   Как мы уже говорили, хук может вызываться только в верхней части функционального компонента и не может использоваться в циклах, условиях или обычных функциях. Как мы упоминали ранее, для компонентов класса очень проблематично совместно использовать логику состояния, и очень обременительно использовать реквизиты рендеринга и HOC. Напротив, React позволяет нам создавать собственные хуки для инкапсуляции логики общего состояния. Так называемый пользовательский хук относится к имени функции, начинающемуся сuseЗапускайте и вызывайте другие функции Hook. Мы используем пользовательский хук, чтобы переписать исходный пример пользовательского состояния с подпиской:

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

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

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

    return isOnline;
}

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

Мы переписали предыдущий пример онлайн-статуса подписанного пользователя с пользовательским хуком.По сравнению со сложной логикой рендеринга и HOC, пользовательский хук более лаконичен.Случай ада компонентной оболочки, который был посещен. Он элегантно решает проблему повторного использования логики состояния для компонентов класса.

Суммировать

   С помощью хуков функциональные компоненты могут в основном реализовать функции большинства компонентов класса.Кроме того, хуки имеют определенные преимущества в совместном использовании логики состояния и улучшении ремонтопригодности компонентов. Можно предвидеть, что хуки, вероятно, станут направлением обозримого будущего React. React официально принял Стратегию постепенного внедрения хуков и заявил, что в настоящее время нет планов по удалению классов из React.Видно, что хуки будут работать параллельно с нашим существующим кодом в течение длительного времени.React не рекомендует нас Все предыдущие Компоненты класса переписываются с помощью хуков, но рекомендуется использовать хуки в новых или некритических компонентах.     Если в выражении есть неточности, прошу смиренно принять критику и совет. Желаем всем вместе прогрессировать!