Получение данных в React — это один процесс, а вот их хранение и кэширование — совсем другой. Возможности кажутся безграничными, а отличия зачастую настолько тонкие, что выбрать правильную технику становится не так уж и просто.
В статье мы рассмотрим различные техники, обращая внимание на все их нюансы и неуловимые различия. Что выбрать: useMemo
или мемоизацию, хранение данных с помощью useState
или контекста? Изучив материал, вы сможете принимать осознанные решения относительно кэширования данных и будете владеть подробной информацией по данной теме. А еще вас ждет много GIF-анимации.
Итак, приступим!
Данные
Перед тем, как углубиться в код, быстро просмотрим данные, которые нам предстоит получать в большинстве компонентов. Файл, выступающий в роли API, выглядит следующим образом:
export default function handler(req, res) {
setTimeout(
() =>
res.status(200).json({
randomNumber: Math.round(Math.random() * 10000),
people: [
{ name: "John Doe" },
{ name: "Olive Yew" },
{ name: "Allie Grater" },
],
}),
750
);
}
Этот код выполняется при осуществлении запроса к пути проекта /api/people
. Как видно, мы возвращаем объект с двумя свойствами:
randomNumber
: произвольное число в диапазоне 0–10000.people
: статический массив с тремя вымышленными именами.
Свойство randomNumber
поможет наглядно продемонстрировать, происходит ли отображение кэшированных данных во фронтенде. По мере углубления в тему вы поймете, о чем идет речь.
Обратите внимание, что мы имитируем небольшую сетевую задержку, используя setTimeout
.
Компонент People
Отображая данные из API, мы передаем их в компонент с именем PeopleRenderer
. Выглядит это следующим образом:
Имея в виду эту вводную информацию, рассмотрим первую технику.
useEffect
Внутри компонентов для получения данных можно задействовать хук useEffect
, после чего сохранять эти данные локально (внутри компонента) с помощью useState
:
import { useEffect, useState } from "react";
import PeopleRenderer from "../PeopleRenderer";
export default function PeopleUseEffect() {
const [json, setJson] = useState(null);
useEffect(() => {
fetch("/api/people")
.then((response) => response.json())
.then((data) => setJson(data));
}, []);
return <PeopleRenderer json={json} />;
}
При передаче пустого массива в качестве второго параметра (строка 11) хук useEffect
будет выполнен после встраивания компонента в DOM. При новом отображении компонента повторно он выполняться уже не будет. Назовем этот хук “одноразовым”.
В связи с применением useEffect
таким способом следует упомянуть о том, что при наличии нескольких экземпляров компонента в DOM все они будут получать данные по отдельности (при встраивании).
В этой технике нет ничего плохого. Более того, иногда она приходится как нельзя кстати. Но в ряде случаев требуется единожды получить данные и повторно их использовать во всех других экземплярах посредством кэширования. С этой целью можно обратиться к другим техникам.
Мемоизация
Мемоизация — это мудреный термин для обозначения очень простой техники. Суть ее в том, что вы создаете функцию, которая при каждом вызове сохраняет результаты в кэше, прежде чем их вернуть.
При первом вызове такой мемоизованной функции результаты вычисляются (или получаются — все зависит от выполняемых операций внутри тела функции). Прежде чем вернуть результаты, вы сохраняете их в кэше в ключе, который создается с входными параметрами:
const MyMemoizedFunction = (age) => {
if(cache.hasKey(age)) {
return cache.get(age);
}
const value = `You are ${age} years old!`;
cache.set(age, value);
return value;
}
Написание такого шаблонного кода вскоре может стать трудоемким процессом, поэтому такие известные библиотеки, как Lodash и Underscore, предоставляют вспомогательные функции, упрощая создание мемоизованных:
import memoize from "lodash.memoize";
const MyFunction = (age) => {
return `You are ${age} years old!`;
}
const MyMemoizedFunction = memoize(MyFunction);
Мемоизация для получения данных
Данная техника подходит для получения данных. Мы создаем функцию getData
, возвращающую обещание Promise
, разрешение которого происходит по окончании fetch-запроса. Мы сохраняем этот Promise
:
import memoize from "lodash.memoize";
const getData = () =>
new Promise((resolve) => {
fetch("http://localhost:3000/api/people")
.then((response) => response.json())
.then((data) => resolve(data));
});
export default memoize(getData);
Обратите внимание, что в данном примере мы не проводим обработку ошибок. Эта тема заслуживает отдельной статьи, особенно в связи с мемоизацией (поскольку отклоненное обещание Promise
тоже бы сохранилось, что чревато проблемами).
Теперь заменим хук useEffect
на другой, который выглядит следующим образом:
import { useEffect, useState } from "react";
import getData from "./getData";
import PeopleRenderer from "../PeopleRenderer";
export default function PeopleMemoize() {
const [json, setJson] = useState(null);
useEffect(() => {
getData().then((data) => setJson(data));
}, []);
return <PeopleRenderer json={json} />;
}
Поскольку результат getData
сохраняется, то при встраивании все компоненты получат одинаковые данные:
Стоит также отметить, что при открытии страницы memoize.tsx
(до добавления первого экземпляра компонента) данные уже предварительно получаются. Дело в том, что мы определили функцию getData
в отдельном расположенном в верхней части страницы файле, при загрузке которого создается Promise
.
Можно аннулировать (очистить) кэш мемоизованной функции, присвоив ее свойству cache
нового Cache
.
getData.cache = new memoize.Cache();
В качестве альтернативного варианта можно очистить существующий кэш (это экземпляр Map
).
getData.cache.clear();
Однако данная функциональность характерна лишь для Lodash. Другие же библиотеки требуют иных решений. Ниже представлен наглядный пример очистки кэша в действии.
React Context
React Context — еще один известный и подробно изученный инструмент (в отношении которого однако часто происходит недопонимание). В связи с этим в очередной раз напоминаю, что он не заменяет такие инструменты, как Redux, поскольку не является средством управления состоянием.
Итак, что такое Context? Это механизм для внедрения данных в дерево компонентов. Если у вас есть некие данные, то их можно сохранить, к примеру, с помощью хука useState
, внутри компонента, находящегося на более высоком уровне иерархии. Затем с помощью Provider
Context внедрить эти данные в дерево, что позволит считывать (использовать) их в любом нижестоящем компоненте.
Для большей ясности приведем пример. Сначала создаем новый контекст.
import { createContext } from "react";
export const PeopleContext = createContext(null);
После этого обертываем компонент, отображающий компоненты People
, в Provider
Context.
export default function Context() {
const [json, setJson] = useState(null);
useEffect(() => {
fetch("/api/people")
.then((response) => response.json())
.then((data) => setJson(data));
}, []);
return (
<PeopleContext.Provider value={{ json }}>
...
</PeopleContext.Provider>
);
}
В 12 строке у нас есть возможность отобразить любой элемент. В определенный момент, опускаясь вниз по дереву, отобразим компонент(ы) People
.
import { useContext } from "react";
import PeopleRenderer from "../PeopleRenderer";
import { PeopleContext } from "./context";
export default function PeopleWithContext() {
const { json } = useContext(PeopleContext);
return <PeopleRenderer json={json} />;
}
Можно применить значение из Provider
с помощью хука useContext
, получая следующий результат.
Обратите внимание на одно важное здесь отличие! На последнем этапе анимации мы нажимаем кнопку “Set new seed”. При этом повторно получаются данные, хранящиеся в Provider
Context. По завершении этого (спустя 750 мс) полученные данные становятся новым значением Provider
, и компоненты People
отображаются еще раз. Как видите, все они пользуются одними и теми же общими данными.
Эта техника значительно отличается от ранее рассмотренного примера мемоизации, в котором каждый компонент хранил собственную копию мемоизованных данных с помощью useState
. В этом же случае, задействуя контекст, они не хранят копии, а оперируют только ссылками на один и тот же объект. Вот почему все компоненты обновляются одинаковыми данными при обновлении значения в Provider
.
useMemo
В последнем, но не маловажном разделе, проведем беглый обзор useMemo
. От предыдущих техник этот хук отличается тем, что он является лишь формой кэширования на локальном уровне: внутри одного экземпляра компонента. useMemo
не предназначен для совместного использования данных несколькими компонентами — по крайней мере, не без обходных решений в виде пробрасывания (prop-drilling) пропсов или внедрения зависимостей (например, React Context).
useMemo
— это инструмент оптимизации. Он позволяет избежать пересчета значения при каждом повторном отображении компонента. Нет лучшего объяснения, чем сама документация, но рассмотрим пример.
export default function Memo() {
const getRnd = () => Math.round(Math.random() * 10000);
const [age, setAge] = useState(24);
const [randomNumber, setRandomNumber] = useState(getRnd());
const pow = useMemo(() => Math.pow(age, 2) + getRnd(), [age]);
return (
...
);
}
getRnd
(строка 2): функция, возвращающая случайное число в диапазоне 0–10000.age
(строка 4): с помощьюuseState
сохраняет число, обозначающее возраст.randomNumber
(строка 5): сохраняет случайное число посредствомuseState
.
И наконец, в строке 7 применяется useMemo
. Мы запоминаем результат вызова функции и сохраняем его в переменной pow
. Функция возвращает значение суммы age
, возведенной во вторую степень, и случайного числа. Поскольку она зависит от переменной age
, мы передаем эту переменную в аргумент зависимости вызова useMemo
.
Функция выполняется только в случае изменения значения age
. Если компонент отображается повторно, а значение age
не изменилось, useMemo
просто вернет мемоизованный результат.
В этом примере вычисление pow
не представляет большой сложности, но не трудно представить себе все преимущества данного подхода в случае с более громоздкой функцией и при необходимости частого повторного отображения компонента.
Две последние анимации показывают, что происходит. Сначала мы обновляем randomNumber
, не затрагивая значение age
, вследствие чего наблюдаем useMemo
в действии (значение pow
не меняется при повторном отображении компонента). При каждом нажатии на кнопку компонент заново отображается.
Однако изменение значения age
повлечет за собой изменение pow
, поскольку вызов useMemo
зависит от значения age
.
Заключение
Для кэширования данных в JavaScript существуют разные техники и вспомогательные средства. В данной статье мы прошлись по самым верхам, тем не менее надеюсь, что полученные знания помогут вам в дальнейшем продвижении в мире разработки.
С полным вариантом кода, представленного в статье, можно ознакомиться в репозитории на GitLab.
Спасибо за внимание!
Читайте также:
- 3 способа улучшить управление состоянием в React
- Как сделать приложение с дополненной реальностью, используя React Native
- Управляйте приложением React с помощью голоса
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Gerard van der Put: Exploring Caching Techniques in React