Хуки useTransition и useDeferredValue в ReactJS 18

2 года назад·3 мин. на чтение

В React 18, релиз которого произошел в марте 2022, появилось много новых инструментов для написания производительных и отзывчивых приложений. Одним из заметных изменений является механизм рендеринга с новой ключевой концепцией: конкурентный рендеринг (concurrent rendering).

В этой статье повнимательнее рассмотрим два новых хука: useTransition() и useDeferredValue(). Эти два хука дают возможность определять приоритет обновления состояния, или, скорее, указывать, является ли обновление менее важным, чем другие, и откладывать его в пользу более срочных.
Какое обновление можно считать срочным, а какое обычным?
  • Срочные обновления: отражают прямое взаимодействие, такое как набор текста, клики, нажатия и т. д., т.е. то с чем взаимодействует пользователь. Когда вы вводите текст в поле ввода, вы хотите сразу увидеть введенный вами текст. В противном случае UI будет казаться медленным и подлагивать. Поэтому мы хотим сделать такие обновления приоритетным.
  • Обычные обновления: переход пользовательского интерфейса из одного вида в другой. Пользователи знают, что представление должно измениться или обновиться (например, когда ожидается ответ на запрос данных). Даже если есть небольшая задержка, это можно рассматривать как ожидаемое поведение, и это не будет восприниматься как медлительность приложения.
Итак, теперь подробнее рассмотрим эти два новых хука, объясним, когда их можно использовать, и посмотрим на конкретные примеры того, как их реализовать.

Хук useTransition() и функция startTransition()

До React 18 все обновления состояния помечались как "срочные". Это означает, что все обновления состояния обрабатывались одинаково с одинаковым приоритетом. С помощью useTransition() теперь можно пометить некоторые обновления состояния как несрочные.

Когда использовать useTransition() ?

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

Как использовать useTransition() ?

function App() {
 const [isPending, startTransition] = useTransition();
 const [searchQuery, setSearchQuery] = useState('');
 
 // запрос данных, который занимает некоторое время
 const filteredResults = getProducts(searchQuery);
 
 function handleQueryChange(event) {
   startTransition(() => {
     // оборачивая setSearchQuery() в startTransition(),
     // мы помечаем эти обновления как менее важные
     setSearchQuery(event.target.value);
   });
 }
 
 return (
   <div>
     <input type="text" onChange={handleQueryChange} />
 
     {isPending && <span>Loading...</span>}
     <ProductsList results={filteredResults} />
   </div>
 );
}

Хук useDeferredValue()

useDeferredValue() очень похож на useTransition() в том, что он позволяет отложить несрочное обновление состояния, но применяется его к части дерева. Это похоже методы debounce и throttle, которые мы часто используем для отложенных обновлений. React будет работать с такими обновлениями, как только завершатся срочные обновления.

Когда использовать useDeferredValue()?

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

Как использовать useDeferredValue() ?

function ProductsList({ results }) {
 // deferredResults получат обновленные данные
 // когда завершатся срочные обновления
 const deferredResults = useDeferredValue(results);
 
 return (
   <ul>
     {deferredResults.map((product) => (
       <li key={product.id}>{product.title}</li>
     ))}
   </ul>
 );
}

Итоги

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

11 ошибок, которых следует избегать при создании React приложений

2 года назад·7 мин. на чтение

Некоторые распространенные ошибки в разработке React приложений, которых следует избегать.

Поскольку React становится все более и более популярным, все больше и больше React разработчиков сталкиваются с различными проблемами в процессе разработки. В этой статье, мы обобщим некоторые распространенные ошибки в разработке React приложений, чтобы помочь вам их избежать. Если вы только начинаете использовать React, рекомендуется внимательно ознакомиться с этой статьей. Если вы уже используете React для разработки проектов, также рекомендуется проверить и заполнить пробелы.
Прочитав эту статью, вы узнаете, как избежать эти 11 ошибок React:
  • При рендеринге списка не используется key
  • Изменение значения состояния прямым присваиванием
  • Привязка значения состояния непосредственно к свойству value инпута
  • Использование состояния сразу после выполнения setState
  • Появление бесконечного цикла при использовании useState + useEffect
  • Отсутствие очистки побочных эффектов в useEffect
  • Неправильное использование логических операторов
  • Тип пропсов компонента не типизирован
  • Передача строк в качестве значений компонентам
  • Имя компонента не начинается с заглавной буквы
  • Неверная привязка события к элементу

Ошибка: При рендеринге списка не используется key

Проблема Когда мы впервые изучали React, мы отображали список следующим образом:
const items = [
  { id: 1, value: 'item1' },
  { id: 2, value: 'item2' },
  { id: 3, value: 'item3' },
  { id: 4, value: 'item4' },
  { id: 5, value: 'item5' }
];

const listItems = items.map((item) => {
  return <li>{item.value}</li>
});
После рендеринга консоль выдаст предупреждение, что для элементов списка необходимо указать ключ. Решение Вам просто нужно последовать этой подсказке и добавить key к каждому элементу:
const items = [
  { id: 1, value: ‘item1’ },
  { id: 2, value: ‘item2’ },
  { id: 3, value: ‘item3’ },
  { id: 4, value: ‘item4’ },
  { id: 5, value: ‘item5’ }
];

const listItems = items.map((item) => {
  return <li key={item.id}>{item.value}</li>
});
key помогает React определить, какие элементы были изменены, например, добавлены или удалены. Поэтому нам нужно установить уникальное значение ключа для каждого элемента в массиве. Для значения ключа лучше всего установить уникальное значение. В приведенном выше примере используется id. Можно использовать индекс массива, но такой подход не рекомендуется. Уникальный ключ помогает React следить за изменениями списка - какой элемент удалился или переместился.

Ошибка: Изменение значения состояния прямым присваиванием

Проблема В React нельзя назначать состояние и изменять напрямую, иначе это вызовет проблемы.
// классовый компонент

handleChange = () => {
   this.state.name = "John";
};
В этот момент будет выдано предупреждение не изменять состояние напрямую, а использовать setState(). Решение Классовые компоненты могут быть изменены с помощью setState(), а функциональные компоненты могут быть изменены с помощью useState():
// Классовые компоненты: используйте setState()
this.setState({ name: "John" });

// Функциональные компоненты:используйте useState()
const [name, setName] = useState("");
setName("John");

Ошибка: Привязка значения состояния непосредственно к свойству value инпута

Проблема Когда мы напрямую привязываем значение состояния к свойству value инпута, мы обнаружим, что независимо от того, что мы вводим в поле ввода, содержимое поля ввода не изменится.
export default function App() {
  const [count, setCount] = useState(0);
  return <input type="text" value={count} />;
}
Это связано с тем, что мы используем переменную состояния в качестве значения по умолчанию для присвоения значения <input>, а состояние в функциональном компоненте может быть изменено только функцией set*, возвращаемым useState. Таким образом, решение также очень простое, просто используйте функцию set* при изменении. Подробнее о том как работать с инпутом в React можно прочитать в этой статье. Решение Просто привяжите событие onChange к <input> и измените его, вызвав setCount:
export default function App() {
  const [count, setCount] = useState(0);
  const handleChange= (event) => setCount(event.target.value);

  return <input type="text" value={count} onChange={handleChange} />;
}

Ошибка: Использование состояния сразу после выполнения setState

Проблема Когда мы изменяем данные через setState() и сразу же хотим получить новые данные, возникнет ситуация, что возвращаются старые данные:
// Классовые компоненты

// инициализация состояния
this.state = { name: "John" };

// обновление состояния
this.setState({ name: "Hello, John!" });
console.log(this.state.name); // => John
Это связано с тем, что setState() является асинхронным. Когда setState() выполняется, реальная операция обновления будет помещена в асинхронную очередь для выполнения, а код, который будет выполняться следующим (т.е. console.log в примере), выполняется синхронно, поэтому выводимое в консоль состояние не является последним значением. Решение Просто передайте последующую операцию, которая будет выполняться как функция, в качестве второго параметра setState(), эта функция обратного вызова будет выполнена после завершения обновления.
this.setState({ name: "Hello, John!" }, () => {
  console.log(this.state.name); // => Hello, John!
});
Теперь обновленное значение выводится правильно.

Ошибка: Появление бесконечного цикла при использовании useState + useEffect

Проблема Когда мы напрямую вызываем метод set*(), возвращаемый useState() внутри useEffect(), и не устанавливаем второй параметр в useEffect(), мы столкнемся с бесконечным циклом:
export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1);
  });
  return <div className="App">{count}</div>;
}
После этого можно увидеть, что данные на странице обновляются, и функция useEffect() вызывается бесконечно, входя в состояние бесконечного цикла. Решение Это распространенная проблема неправильного использования useEffect(). useEffect() можно рассматривать как комбинацию трех функций жизненного цикла: componentDidMount, componentDidUpdate и componentWillUnmount в классовых компонентах. useEffect(effect, deps) принимает 2 аргумента:
  • effect функция, которая должна выполниться (побочный эффект)
  • deps массив зависимостей
При изменении массива deps выполняется функция эффекта. Чтобы изменить метод, вам нужно всего лишь передать [] в качестве второго аргумента useEffect() :
export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1);
  }, []);

  return <div className="App">{count}</div>;
}
Приведем 4 случая использования useEffect:
  • Если второй параметр не передан: при обновлении любого состояния будет запущена функция эффекта useEffect.
useEffect(() => {
  setCount(count + 1);
});
  • Если второй параметр - это пустой массив: функция эффекта useEffect срабатывает только при монтировании и размонтировании.
useEffect(() => {
  setCount(count + 1);
}, []);
  • Если второй параметр представляет собой массив с одним значением: функция эффекта useEffect будет запускаться только при изменении значения.
useEffect(() => {
  setCount(count + 1);
}, [name]);
  • Если второй параметр представляет собой массив c несколькими значениями: функция эффекта useEffect будет запускаться при изменении хотя бы одного из значений из списка зависимостей.
useEffect(() => {
  setCount(count + 1);
}, [name, age]);

Ошибка: Отсутствие очистки побочных эффектов в useEffect

Проблема В классовых компонентах мы используем метод жизненного цикла componentWillUnmount() для очистки некоторых побочных эффектов, таких как таймеры, слушатели событий и т. д. Решение Из функции эффекта useEffect() может быть возвращена функция очистки, которая аналогична роли метода жизненного цикла componentWillUnmount():
useEffect(() => {
  // ...
  return () => clearInterval(id);
}, [name, age]);

Ошибка: Неправильное использование логических операторов

Проблема В синтаксисе JSX/TSX мы часто используем логические значения для управления отображаемыми элементами, и во многих случаях мы используем оператор && для обработки этой логики:
const count = 0;
const Comp = () => count && <h1>Chris1993</h1>;
Мы думаем, что в это время страница будет отображать пустой контент, но на самом деле на ней отобразится 0. Решение Причина в том, что ложное выражение приводит к тому, что элементы после && пропускаются, и будет возвращено значение ложного выражения. Поэтому нужно стараться написать условие оценки как можно более полным, не полагаясь на истинное и ложное логическое значение JavaScript для сравнения:
const count = 0;
const Comp = () => count > 0 && <h1>Chris1993</h1>;
Теперь страница будет отображать пустой контент, как и ожидается.

Ошибка: Типы просов компонента не типизированы

Проблема Если компоненты, разработанные разными членами команды, не имеют четко определенных типов для просов, то для коллег будет не очевидно, как использовать компоненты, например:
const UserInfo = (props) => {
  return (
    <div>
      {props.name} : {props.age}
    </div>
  );
};
Решение
  • Определить типы пропсов компонента, используя TypeScript.
// Классовые компоненты
interface AppProps {
  value: string;
}
interface AppState {
  count: number;
}
class App extends React.Component<AppProps, AppStore> {
  // ...
}

// Функциональные компоненты
interface AppProps {
  value?: string;
}
const App: React.FC<AppProps> = ({ value = "", children }) => {
  //...
};
  • Без использования TypeScript типы пропсов могут быть определены с помощью propTypes.
const UserInfo = (props) => {
  return (
    <div>
      {props.name} : {props.age}
    </div>
  );
};
UserInfo.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired,
};

Ошибка: Передача строк в качестве значений компонентам

Проблема Так как React имеет шаблонный синтаксис, очень похожий на HTML, часто бывает так, что числа передаются напрямую компонентам в пропсы, что приводит к неожиданному результату:
<MyComp count="99"></MyComp>
Сравнение props.count === 99 в компоненте MyComp вернет false. Решение Правильный способ должен заключаться в использовании фигурных скобок для передачи пропсов:
<MyComp count={99}></MyComp>
Передача строковых просов будет выглядеть следующим образом:
<MyComp count={"99"}></MyComp>

Ошибка: Имя компонента не начинается с заглавной буквы

Проблема Начинающие разработчики часто забывают называть свои компоненты с заглавной буквы. Компоненты, начинающиеся со строчной буквы в JSX/TSX, компилируются в элементы HTML, такие как <div /> для тегов HTML.
class myComponent extends React.component {}
Решение Просто измените первую букву на заглавную:
class MyComponent extends React.component {}

Ошибка: Неверная привязка события к элементу в классовых компонентах

Проблема
import { Component } from "react";

export default class HelloComponent extends Component {
  constructor() {
    super();
    this.state = {
      name: "John",
    };
  }
  update() {
    this.setState({ name: "Hello John!" });
  }

  render() {
    return (
      <div>
        <button onClick={this.update}>update</button>
      </div>
    );
  }
}
При нажатии на кнопку update консоль сообщит об ошибке, что невозможно прочитать свойства undefined (чтение setState) Решение Это происходит потому, что this не привязан к тому контексту, который мы ожидаем. Есть несколько решений:
  • Привязать контекст в конструкторе при помощи метода bind
constructor() {
  super();
  this.state = {
    name: "John"
  };
  this.update = this.update.bind(this);
}
  • Использовать стрелочные функции
update = () => {
  this.setState({ name: "Hello John!" });
};
  • Привязать прямо в функции рендеринга
<button onClick={this.update.bind(this)}>update</button>
  • Использовать стрелочные функции в функции рендеринга (не рекомендуется, т.к. это создает новую функцию каждый раз при рендеринге компонента, что влияет на производительность)
<button onClick={() => this.update()}>update</button>