Как стать автором
Обновить

Вещи, которые полезно знать о React.js

Разработка веб-сайтов *JavaScript *Программирование *Проектирование и рефакторинг *ReactJS *
Tutorial

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

Возвращаемая функция для сброса в useEffect срабатывает чаще чем вы думаете

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

Пример на codesandbox

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

  • в componentDidMount установили бы таймер

componentDidMount() {
    const { stateForTimer } = this.state;
    this.timer = setTimeout(() => console.log(stateForTimer), 1000);
}
  • в componentDidUpdate сбрасывали бы этот таймер и устанавливали заново, если мы это не сделаем, в таймере будет неактуальное состояние.

componentDidUpdate() {
    const { stateForTimer } = this.state;
    clearTimeout(this.timer);
    this.timer = setTimeout(() => console.log(stateForTimer), 1000);
}
  • в componentWillUnmount мы отписываемся от таймера, тк не хотим, чтобы он сработал, если у нас уже нет компонента на экране.

componentWillUnmount() {
    clearTimeout(this.timer);
}
  • Но таким образом наш таймер будет отрабатывать при каждом ререндере, в родительском компоненте, чтобы этого не происходило мы можем вместо Component использовать PureComponent

export default class ClassComponentTimer extends React.PureComponent {
  constructor() {
    super();
    this.state = { stateForTimer: "state For Timer" };
    this.timer = null;
  }
  componentDidMount() {
    const { stateForTimer } = this.state;
    this.timer = setTimeout(() => console.log(stateForTimer), 1000);
  }

  componentDidUpdate() {
    const { stateForTimer } = this.state;
    clearTimeout(this.timer);
    this.timer = setTimeout(() => console.log(stateForTimer), 1000);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  inputHandler = (e) => {
    this.setState({ stateForTimer: e.target.value });
  };

  render() {
    return (
      <div className="class">
        <h1>Class timer</h1>
        <input value={this.state.stateForTimer} onChange={this.inputHandler} />
      </div>
    );
  }
}

Согласитесь, получилось довольно многословно.

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

export default function App() {
  const [stateForTimer, setStateForTimer] = useState("state For Timer");

  useEffect(() => {
    const timeout = setTimeout(() => console.log(stateForTimer), 1000);
    return () => clearTimeout(timeout);
  }, [stateForTimer]);

  const inputHandler = (e) => {
    setStateForTimer(e.target.value);
  };

  return (
    <div className="func">
      <h1>Function timer</h1>
      <input value={stateForTimer} onChange={inputHandler} />
    </div>
  );
}

В плане лаконичности компоненты на хуках конечно выигрывают.

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

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

пример на codesandbox

Это значит, что какие-то сложные вычисления не стоит делать внутри компонента напрямую, а сайдэфекты вообще нельзя, потому что они будут срабатывать бесконтрольно, лучше сделать это в useEffect или в useMemo.

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

Вызывать функции в useState будет отрабатывать на каждый ререндер

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

пример на codesandbox

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

import { useState } from "react";

const twoSquared = () => {
  console.log("do some maths once");
  return 3 * 3;
};

export default function StateOnce() {
  const [nine] = useState(() => twoSquared());

  return (
    <div className="every render">
      <p> 3 * 3 = {nine} </p>
    </div>
  );
}

setState в функциональных компонентах может принимать функцию

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

Выведится 5 раз 5
for (let i = 1; i <= 5; i++) {
  setCount(count + 1);
}

Выведится от 1 до 5
for (let i = 1; i <= 5; i++) {
	setCount((prevCount) => prevCount + 1);
}

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

Пример кода на codesandbox

Помимо useEffect есть еще спецефичный хук useLayoutEffect

useLayoutEffect так же как и useEffect сработает после рендера, но в отличии от useEffect срабатывает он синхронно.

Это значит, что при использовании useEffect у браузера будет какое-то время для отрисовки контента, а значит, что если мы пытаемся что-то отрендерить, а в useEffect мы например следим за тем, что пытаемся отрендерить и хотим перехватить это и отрисовать вместо этого какое-то другое значение, то мы увидем небольшое моргание. Но лучше один раз увидеть пример, чем сто раз прочитать его описание.

Пример кода на codesandbox

в примере из codesandbox потыкайте много раз подряд на кнопку «Get name with delay», вы увидите небольшое мерцание.

Тут происходит следующее:

  1. По нажатию на кнопку мы устанавливаем Имя в состояние компонента, оно записывается в виртуальный DOM

  2. Запускается useEffect

  3. Код внутри него откладывается в асинхронную очередь

  4. js завершает все операции что лежат в стэке вызовов (привет Event loop), в том числе отображение пользователю того, что мы записали в состояние компонента по нажатию на кнопку.

  5. После того, как стэк вызовов опустел, мы переходим к очереди, в которой и лежит наш колбэк из useEffect

  6. Мы его выполняем, меняется виртуальное дерево

  7. Рендерится уже то, что мы задали в состояние внутри этого колбэка.

useLayoutEffect же вместо того, чтобы отправлять колбэк в асинхронную очередь, сразу же выполняет код, ну точнее не совсем сразу, сначала отрабатывает setState из кнопки, идет изменение в виртуальном дом дереве, после этого отрабатывает useLayoutEffect, но он в отличии от useEffect не откладывает колбэк в асинхронную очередь вызовов, а выполняет его сразу, еще раз меняется виртуальное ДОМ дерево и после этого уже происходит ререндер реального браузерного дерева. Попробуйте быстро потыкать кнопку «Get name without delay», моргания вы не увидите.

Аккуратно с 0 при проверке на необходимость рендеринга

Напоследок минипредостережение. В обычном JS коде мы часто делаем такую проверку: 

if(arr.length) {
  //если длинна массива !== 0, работаем с элементами массива
  arr.map((item) => {
 		//ваш код
  })
}

И кажется логичным сделать такую же проверку в рендере компонента:

items.length && items.map((item) => <div>{item}</div>)

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

Для такого случая можно использовать такую проверку:

import "./styles.css";

const items = [];

export default function App() {
  return (
    <div className="App">
      <h2>Check array on arr.length:</h2>
      {items.length && items.map((item) => <div>{item}</div>)}
      <h2>Check array on arr.length {">"} 0:</h2>
      {items.length > 0 && items.map((item) => <div>{item}</div>)}
    </div>
  );
}

Надеюсь эта статья была кому-нибудь полезна. Если вы увидели какие-то ошибки, буду рад, если вы напишете о них, я обязательно внесу исправления.

UPD:

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

  • В последнем пункте, я использовал не совсем подходящий пример:
    {items.length > 0 && items.map((item) => {item})}
    это бессмысленно, можно сразу проходиться по массиву items и если его длинна равна нулю, то ничего не отрендерится.
    Но это не значит, что не нужно следить за проверкой на 0. Может быть такая ситуация, что если у вас есть массив элементов длина которога равна нулю, то вам не нужно рендерить другой кусок верстки, например какую-нибудь кнопку. Тогда вместо такой проверки: {items.length && <button>Кнопка</button>} (которая отрендерит 0, если длинна равна нулю) стоит использовать такую: {items.length > 0 && <button>Кнопка</button>}

Теги:
Хабы:
Всего голосов 10: ↑8 и ↓2 +6
Просмотры 8.5K
Комментарии Комментарии 10