cache

cache дозволяє кешувати результат вибірки даних або обчислень.

const cachedFn = cache(fn);

Довідник

cache(fn)

Викличте cache поза будь-якими компонентами, щоб створити версію функції з кешуванням.

import {cache} from 'react';
import calculateMetrics from 'lib/metrics';

const getMetrics = cache(calculateMetrics);

function Chart({data}) {
  const report = getMetrics(data);
  // ...
}

Коли getMetrics вперше викликається з даними, getMetrics викличе calculateMetrics(data) і збереже результат у кеші. Якщо getMetrics буде викликано повторно з тими самими даними , він поверне кешований результат замість повторного виклику calculateMetrics(data).

Дивіться більше прикладів нижче.

Параметри

  • fn: Функція, для якої потрібно кешувати результати. fn може приймати будь-які аргументи і повертати будь-яке значення.

Повернення

cache повертає кешовану версію fn з тією ж сигнатурою типу. Вона не викликає fn у процесі виконання.

При виклику cachedFn з заданими аргументами, він спочатку перевіряє, чи існує кешований результат в кеші. Якщо кешований результат існує, повертається результат. Якщо ні, вона викликає fn з аргументами, зберігає результат у кеші і повертає результат. Єдиний раз, коли викликається fn, це коли є помилка кешу.

Оптимізація кешування значень, що повертаються, на основі вхідних даних відома як мемоїзація. Ми називаємо функцію, повернуту з кешу, запам'ятовуваною функцією.

Застереження

  • React буде анулювати кеш для всіх запам'ятовуваних функцій для кожного запиту сервера.
  • Кожен виклик cache створює нову функцію. Це означає, що виклик cache з однією і тією ж функцією декілька разів поверне різні запам'ятовувані функції, які не мають спільного кешу.
  • cachedFn також кешує помилки. Якщо fn видасть помилку для певних аргументів, її буде кешовано, і ця ж помилка буде повторно видана при виклику cachedFn з тими ж аргументами.
  • кеш призначено лише для використання у Компонентах сервера React.

Використання

Кешування складних обчислень

Використовуйте кеш для уникнення дублювання роботи.

import {cache} from 'react';
import calculateUserMetrics from 'lib/user';

const getUserMetrics = cache(calculateUserMetrics);

function Profile({user}) {
  const metrics = getUserMetrics(user);
  // ...
}

function TeamReport({users}) {
  for (let user in users) {
    const metrics = getUserMetrics(user);
    // ...
  }
  // ...
}

Якщо один і той самий об'єкт user рендериться і в Profile, і в TeamReport, ці два компоненти можуть розділити роботу і викликати calculateUserMetrics лише один раз для цього user.

Припустимо, що Profile буде відрендерено першим. Він викличе <code>getUserMetrics , і перевірити, чи є кешований результат. Оскільки getUserMetrics викликається вперше з цим користувачем, буде пропуск кешу. getUserMetrics викличе calculateUserMetrics з цим користувачем і запише результат до кешу.

Коли TeamReport рендерить свій список користувачів і досягає того самого об'єкта user, він викличе <code>getUserMetrics і прочитати результат з кешу.

Виклик різних запам'ятованих функцій зчитуватиме дані з різних кешів.

Щоб отримати доступ до того самого кешу, компоненти повинні викликати ту саму запам'ятовувану функцію.

// Temperature.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export function Temperature({cityData}) {
  // 🚩 Wrong: Calling `cache` in component creates new `getWeekReport` for each render
  const getWeekReport = cache(calculateWeekReport);
  const report = getWeekReport(cityData);
  // ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 Wrong: `getWeekReport` is only accessible for `Precipitation` component.
const getWeekReport = cache(calculateWeekReport);

export function Precipitation({cityData}) {
  const report = getWeekReport(cityData);
  // ...
}

У наведеному вище прикладі <код>Опади та <code>Temperature кожен виклик cache для створення нової запам'ятовуваної функції з власним пошуком у кеші. Якщо обидва компоненти рендеритимуть один і той самий cityData, вони виконуватимуть дублюючу роботу для виклику calculateWeekReport.

Крім того, Temperature створює нову запам'ятовувану функцію </CodeStep> кожного разу при рендерингу компонента, що не дозволяє спільного використання кешу.</p> <p>Щоб максимізувати кількість звернень до кешу та зменшити роботу, два компоненти повинні викликати ту саму функцію, що запам'ятовується, для доступу до того самого кешу. Замість цього визначте функцію, що запам'ятовується, у спеціальному модулі, який може бути <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import" target="_blank" rel="nofollow noopener nooreferrer"><><code>import-ed між компонентами.

// getWeekReport.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export default cache(calculateWeekReport);
// Temperature.js
import getWeekReport from './getWeekReport';

export default function Temperature({cityData}) {
	const report = getWeekReport(cityData);
  // ...
}
// Precipitation.js
import getWeekReport from './getWeekReport';

export default function Precipitation({cityData}) {
  const report = getWeekReport(cityData);
  // ...
}

Тут обидва компоненти викликають одну й ту саму запам'ятовану функцію CodeStep> експортовано з <code>./getWeekReport.js для читання та запису до того ж кешу.

Надайте знімок даних

Щоб поділитися знімком даних між компонентами, викличте cache з функцією отримання даних на зразок fetch. Коли декілька компонентів виконують один і той самий запит, робиться лише один запит, а повернуті дані кешуються і використовуються всіма компонентами. Усі компоненти посилаються на один і той самий знімок даних під час рендерингу на сервері.

import {cache} from 'react';
import {fetchTemperature} from './api.js';

const getTemperature = cache(async (city) => {
	return await fetchTemperature(city);
});

async function AnimatedWeatherCard({city}) {
	const temperature = await getTemperature(city);
	// ...
}

async function MinimalWeatherCard({city}) {
	const temperature = await getTemperature(city);
	// ...
}

Якщо AnimatedWeatherCard і MinimalWeatherCard обидва рендерять для одного і того ж міста</CodeStep> , вони отримають той самий знімок даних з <CodeStep data-step="2">>запам'ятованої функції</CodeStep> .</p> <p>Якщо <code>AnimatedWeatherCard і MinimalWeatherCard надають різні міста</CodeStep> аргументи до <CodeStep data-step="2"><code>getTemperature , то fetchTemperature буде викликано двічі, і кожен сайт виклику отримає різні дані.

місто</CodeStep> діє як ключ кешу.</p> <Примітка><p><CodeStep data-step="3">Асинхронний рендеринг</CodeStep> підтримується тільки для серверних компонентів.</p> <pre><code data-meta="[[3, 1, "async"], [3, 2, "await"]]" class="language-js">async function AnimatedWeatherCard({city}) { const temperature = await getTemperature(city); // ... }

Перезавантажити дані

Кешуючи довготривалу вибірку даних, ви можете запустити асинхронну роботу перед рендерингом компонента.

const getUser = cache(async (id) => {
  return await db.user.query(id);
}

async function Profile({id}) {
  const user = await getUser(id);
  return (
    <section>
      <img src={user.profilePic} />
      <h2>{user.name}</h2>
    </section>
  );
}

function Page({id}) {
  // ✅ Good: start fetching the user data
  getUser(id);
  // ... some computational work
  return (
    <>
      <Profile id={id} />
    </>
  );
}

Під час рендерингу Page компонент викликає <код>getUser , але зауважте, що він не використовує дані, що було повернено. Цей ранній <код>getUser виклик запускає асинхронний запит до бази даних, який відбувається під час виконання Page іншої обчислювальної роботи та рендерингу дочірніх елементів.

Під час рендерингу Profile ми викликаємо <code>getUser знову. Якщо початковий <код>getUser виклик вже повернувся і закешував дані користувача, коли Profile запитує і чекає на ці дані</CodeStep> , він може просто читати з кешу, не вимагаючи іншого віддаленого виклику процедури. Якщо <CodeStep data-step="1">початковий запит даних</CodeStep> не завершено, попереднє завантаження даних у цьому шаблоні зменшує затримку при отриманні даних.</p> <DeepDive><h4 id="caching-asynchronous-work">Кешування асинхронної роботи</h4> <p>При оцінці <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function" target="_blank" rel="nofollow noopener noreferrer">асинхронної функції</a>, ви отримаєте <a href="https://developer. mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" target="_blank" rel="nofollow noopener noreferrer">Обіцянку</a> за цю роботу. Обіцянка містить стан цієї роботи (<em>на розгляді</em>, <em>виконано</em>, <em>не виконано</em>) та її остаточний результат.</p> <p>У цьому прикладі асинхронна функція <CodeStep data-step="1"><code>fetchData повертає обіцянку, яка очікує на fetch.

async function fetchData() {
  return await fetch(`https://...`);
}

const getData = cache(fetchData);

async function MyComponent() {
  getData();
  // ... some computational work  
  await getData();
  // ...
}

При виклику <коду>getData першого разу обіцянка повернулася з <code>fetchData кешується. Наступні пошуки повертатимуть ту саму обіцянку.

Зверніть увагу, що перший <код>getData виклик не чекає тоді як другий </CodeStep> робить. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await" target="_blank" rel="nofollow noopener noreferrer"><code>await - це оператор JavaScript, який буде чекати і поверне усталений результат обіцянки. Перший <код>getData виклик просто ініціює fetch для кешування обіцянки для другого <коду>getData для пошуку.

Якщо при другому виклику </CodeStep> обіцянка все ще <em>pending</em>, тоді <code>await зробить паузу для отримання результату. Оптимізація полягає в тому, що поки ми чекаємо на fetch, React може продовжувати обчислювальну роботу, таким чином зменшуючи час очікування на другий виклик</CodeStep> .</p> <p>Якщо обіцянка вже виконано, або до помилки, або до результату <em>виконано</em>, <code>await негайно поверне це значення. В обох випадках є перевага у продуктивності.

Виклик запам'ятованої функції поза компонентом не використовуватиме кеш.
import {cache} from 'react';

const getUser = cache(async (userId) => {
  return await db.user.query(userId);
});

// 🚩 Wrong: Calling memoized function outside of component will not memoize.
getUser('demo-id');

async function DemoProfile() {
  // ✅ Good: `getUser` will memoize.
  const user = await getUser('demo-id');
  return <Profile user={user} />;
}

React надає лише кеш-доступ до запам'ятованої функції в компоненті. При виклику <code>getUser поза компонентом, він все одно буде обчислювати функцію, але не буде читати або оновлювати кеш.

Це тому, що доступ до кешу надається через контекст, який доступний лише з компонента.

Коли слід використовувати cache, memo або useMemo?

Усі згадані API пропонують кешування, але різниця полягає у тому, що саме вони мають намір запам'ятовувати, хто може отримати доступ до кешу та коли їхній кеш стає недійсним.

useMemo

Взагалі, для кешування важких обчислень у клієнтському компоненті між рендерами слід використовувати useMemo. Наприклад, для запам'ятовування перетворення даних всередині компонента.

'use client';

function WeatherReport({record}) {
  const avgTemp = useMemo(() => calculateAvg(record)), record);
  // ...
}

function App() {
  const record = getRecord();
  return (
    <>
      <WeatherReport record={record} />
      <WeatherReport record={record} />
    </>
  );
}

У цьому прикладі App рендерить два WeatherReport з однаковим записом. Хоча обидва компоненти виконують однакову роботу, вони не можуть ділитися нею. Кеш useMemo є локальним лише для компонента.

Однак, useMemo гарантує, що якщо App буде рендеритися повторно і об'єкт record не зміниться, кожен екземпляр компонента пропустить роботу і використає запам'ятоване значення avgTemp. useMemo кешуватиме лише останнє обчислення avgTemp із заданими залежностями.

cache

Загалом, вам слід використовувати кеш у серверних компонентах для запам'ятовування роботи, яку можна спільно використовувати між компонентами.

"], [3, 13, "<WeatherReport city={city} />"], [2, 1, "cache(fetchReport)"]]" class="language-js">const cachedFetchReport = cache(fetchReport);

function WeatherReport({city}) {
  const report = cachedFetchReport(city);
  // ...
}

function App() {
  const city = "Los Angeles";
  return (
    <>
      <WeatherReport city={city} />
      <WeatherReport city={city} />
    </>
  );
}

Переписування попереднього прикладу для використання кешу, у цьому випадку другий екземпляр <коду>WeatherReport зможе пропускати дублюючу роботу і читати з того ж кешу, що і перший <код>WeatherReport . Ще одна відмінність від попереднього прикладу полягає в тому, що кеш також рекомендується для запам'ятовування вибірки даних</CodeStep> , на відміну від <code>useMemo, який слід використовувати лише для обчислень.

Наразі кеш слід використовувати лише у серверних компонентах, і кеш буде недійсним для серверних запитів.

memo

Вам слід використовувати пам'ятку, щоб запобігти повторному відображенню компонента, якщо його пропси не змінено.

'use client';

function WeatherReport({record}) {
  const avgTemp = calculateAvg(record); 
  // ...
}

const MemoWeatherReport = memo(WeatherReport);

function App() {
  const record = getRecord();
  return (
    <>
      <MemoWeatherReport record={record} />
      <MemoWeatherReport record={record} />
    </>
  );
}

У цьому прикладі обидва компоненти MemoWeatherReport викличуть calculateAvg при першому рендері. Однак, якщо App повторно відрендерить без змін запис , жоден з пропсів не зміниться і MemoWeatherReport не буде повторно відрендерений.

У порівнянні з useMemo, memo запам'ятовує рендеринг компонента на основі пропсів, а не конкретних обчислень. Подібно до useMemo, запам'ятований компонент кешує лише останній рендеринг з останніми значеннями пропсів. Щойно пропси змінюються, кеш стає недійсним, і компонент рендериться заново.


Налагодження

Моя запам'ятована функція все ще працює, хоча я викликав її з тими самими аргументами

Дивіться раніше згадані пастки

Якщо жоден з наведених вище варіантів не підходить, це може бути проблемою з тим, як React перевіряє, чи існує щось у кеші.

Якщо ваші аргументи не є примітивами (наприклад, об'єкти, функції, масиви), переконайтеся, що ви передаєте те саме посилання на об'єкт.

При виклику функції, що запам'ятовується, React перегляне вхідні аргументи, щоб побачити, чи результат вже закешований. React буде використовувати неглибоку рівність аргументів, щоб визначити, чи є кешування.

import {cache} from 'react';

const calculateNorm = cache((vector) => {
  // ...
});

function MapMarker(props) {
  // 🚩 Wrong: props is an object that changes every render.
  const length = calculateNorm(props);
  // ...
}

function App() {
  return (
    <>
      <MapMarker x={10} y={10} z={10} />
      <MapMarker x={10} y={10} z={10} />
    </>
  );
}

У цьому випадку два MapMarker виглядають так, ніби вони виконують однакову роботу і викликають calculateNorm з однаковим значенням {x: 10, y: 10, z:10}. Незважаючи на те, що об'єкти містять однакові значення, вони не є одним і тим самим посиланням на об'єкт, оскільки кожен компонент створює власний об'єкт props.

React викличе Object.is на вході, щоб перевірити, чи є попадання в кеш.

import {cache} from 'react';

const calculateNorm = cache((x, y, z) => {
  // ...
});

function MapMarker(props) {
  // ✅ Good: Pass primitives to memoized function
  const length = calculateNorm(props.x, props.y, props.z);
  // ...
}

function App() {
  return (
    <>
      <MapMarker x={10} y={10} z={10} />
      <MapMarker x={10} y={10} z={10} />
    </>
  );
}

Одним із способів вирішення цієї проблеми може бути передача розмірів вектора до calculateNorm. Це працює, оскільки самі розміри є примітивами.

Іншим рішенням може бути передача самого векторного об'єкта як пропу компоненту. Нам потрібно буде передати той самий об'єкт обом екземплярам компонента.

import {cache} from 'react';

const calculateNorm = cache((vector) => {
  // ...
});

function MapMarker(props) {
  // ✅ Good: Pass the same `vector` object
  const length = calculateNorm(props.vector);
  // ...
}

function App() {
  const vector = [10, 10, 10];
  return (
    <>
      <MapMarker vector={vector} />
      <MapMarker vector={vector} />
    </>
  );
}