cache - This feature is available in the latest Canary

Canary

cache permite que você armazene em cache o resultado de uma busca de dados ou computação.

const cachedFn = cache(fn);

Referência

cache(fn)

Chame cache fora de qualquer componente para criar uma versão da função com cache.

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

const getMetrics = cache(calculateMetrics);

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

Quando getMetrics é chamado pela primeira vez com data, getMetrics chamará calculateMetrics(data) e armazenará o resultado em cache. Se getMetrics for chamado novamente com os mesmos data, ele retornará o resultado em cache em vez de chamar calculateMetrics(data) novamente.

Veja mais exemplos abaixo.

Parâmetros

  • fn: A função para a qual você deseja armazenar resultados em cache. fn pode aceitar quaisquer argumentos e retornar qualquer valor.

Retornos

cache retorna uma versão em cache de fn com a mesma assinatura de tipo. Ele não chama fn no processo.

Ao chamar cachedFn com determinados argumentos, ele primeiro verifica se um resultado em cache existe. Se um resultado em cache existir, ele retorna o resultado. Se não, ele chama fn com os argumentos, armazena o resultado em cache e retorna o resultado. A única vez que fn é chamado é quando há uma falha no cache.

Note

A otimização de armazenar em cache valores de retorno com base em entradas é conhecida como memoização. Nós nos referimos à função retornada de cache como uma função memoizada.

Ressalvas

  • O React invalidará o cache para todas as funções memoizadas para cada solicitação do servidor.
  • Cada chamada a cache cria uma nova função. Isso significa que chamar cache com a mesma função várias vezes retornará diferentes funções memoizadas que não compartilham o mesmo cache.
  • cachedFn também armazenará em cache erros. Se fn lançar um erro para determinados argumentos, ele será armazenado em cache, e o mesmo erro será relançado quando cachedFn for chamado com esses mesmos argumentos.
  • cache é para uso apenas em Componentes do Servidor.

Uso

Armazenar em cache uma computação cara

Use cache para evitar trabalho duplicado.

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);
// ...
}
// ...
}

Se o mesmo objeto user for renderizado em ambos Profile e TeamReport, os dois componentes podem compartilhar trabalho e chamar calculateUserMetrics apenas uma vez para aquele user.

Suponha que Profile seja renderizado primeiro. Ele chamará getUserMetrics e verificará se há um resultado em cache. Como é a primeira vez que getUserMetrics é chamado com aquele user, haverá uma falha no cache. getUserMetrics chamará então calculateUserMetrics com aquele user e gravará o resultado em cache.

Quando TeamReport renderiza sua lista de users e chega ao mesmo objeto user, ele chamará getUserMetrics e lerá o resultado do cache.

Pitfall

Chamar diferentes funções memoizadas lerá de caches diferentes.

Para acessar o mesmo cache, os componentes devem chamar a mesma função memoizada.

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

export function Temperature({cityData}) {
// 🚩 Errado: Chamar `cache` no componente cria um novo `getWeekReport` para cada renderização
const getWeekReport = cache(calculateWeekReport);
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 Errado: `getWeekReport` está acessível apenas para o componente `Precipitation`.
const getWeekReport = cache(calculateWeekReport);

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

No exemplo acima, Precipitation e Temperature cada um chama cache para criar uma nova função memoizada com sua própria consulta de cache. Se ambos os componentes renderizarem para o mesmo cityData, eles farão trabalho duplicado ao chamar calculateWeekReport.

Além disso, Temperature cria uma nova função memoizada cada vez que o componente é renderizado, o que não permite nenhum compartilhamento de cache.

Para maximizar os acertos de cache e reduzir o trabalho, os dois componentes devem chamar a mesma função memoizada para acessar o mesmo cache. Em vez disso, defina a função memoizada em um módulo dedicado que pode ser importado entre componentes.

// 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);
// ...
}

Aqui, ambos os componentes chamam a mesma função memoizada exportada de ./getWeekReport.js para ler e gravar no mesmo cache.

Compartilhar um instantâneo de dados

Para compartilhar um instantâneo de dados entre componentes, chame cache com uma função de busca de dados como fetch. Quando vários componentes realizam a mesma busca de dados, apenas uma solicitação é feita e os dados retornados são armazenados em cache e compartilhados entre os componentes. Todos os componentes referem-se ao mesmo instantâneo de dados na renderização do servidor.

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);
// ...
}

Se AnimatedWeatherCard e MinimalWeatherCard ambos renderizarem para o mesmo city, eles receberão o mesmo instantâneo de dados da função memoizada.

Se AnimatedWeatherCard e MinimalWeatherCard fornecerem diferentes argumentos city para getTemperature, então fetchTemperature será chamado duas vezes e cada local de chamada receberá dados diferentes.

O city atua como uma chave de cache.

Note

A renderização assíncrona é suportada apenas para Componentes do Servidor.

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

Pré-carregar dados

Ao armazenar em cache uma busca de dados de longa duração, você pode iniciar o trabalho assíncrono antes de renderizar o componente.

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}) {
// ✅ Bom: comece a buscar os dados do usuário
getUser(id);
// ... algum trabalho computacional
return (
<>
<Profile id={id} />
</>
);
}

Ao renderizar Page, o componente chama getUser mas note que não usa os dados retornados. Esta chamada inicial getUser inicia a consulta assíncrona ao banco de dados que ocorre enquanto Page está fazendo outro trabalho computacional e renderizando filhos.

Ao renderizar Profile, chamamos getUser novamente. Se a chamada inicial getUser já retornou e armazenou em cache os dados do usuário, quando Profile pede e aguarda esses dados, ele pode simplesmente ler do cache sem precisar de outra chamada remota. Se a solicitação de dados inicial não tiver sido concluída, a pré-carga de dados neste padrão reduz o atraso na busca de dados.

Deep Dive

Armazenando em cache o trabalho assíncrono

Ao avaliar uma função assíncrona, você receberá uma Promise para esse trabalho. A promessa mantém o estado desse trabalho (pendente, cumprido, falhou) e seu eventual resultado resolvido.

Neste exemplo, a função assíncrona fetchData retorna uma promessa que está aguardando o fetch.

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

const getData = cache(fetchData);

async function MyComponent() {
getData();
// ... algum trabalho computacional
await getData();
// ...
}

Ao chamar getData pela primeira vez, a promessa retornada de fetchData é armazenada em cache. Busca subsequentes retornarão então a mesma promessa.

Note que a primeira chamada getData não await, enquanto a segunda sim. await é um operador JavaScript que aguardará e retornará o resultado resolvido da promessa. A primeira chamada getData simplesmente inicia o fetch para armazenar a promessa para que a segunda getData a consulte.

Se na segunda chamada a promessa ainda estiver pendente, então await irá pausar para o resultado. A otimização é que enquanto aguardamos o fetch, o React pode continuar com o trabalho computacional, reduzindo assim o tempo de espera para a segunda chamada.

Se a promessa já estiver resolvida, seja para um erro ou para o resultado cumprido, await retornará esse valor imediatamente. Em ambos os resultados, há um benefício de desempenho.

Pitfall

Chamar uma função memoizada fora de um componente não usará o cache.
import {cache} from 'react';

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

// 🚩 Errado: Chamar função memoizada fora do componente não fará a memoização.
getUser('demo-id');

async function DemoProfile() {
// ✅ Bom: `getUser` fará a memoização.
const user = await getUser('demo-id');
return <Profile user={user} />;
}

O React só fornece acesso ao cache da função memoizada em um componente. Ao chamar getUser fora de um componente, ele ainda avaliará a função, mas não lerá ou atualizará o cache.

Isso ocorre porque o acesso ao cache é fornecido através de um contexto que só é acessível a partir de um componente.

Deep Dive

Quando devo usar cache, memo ou useMemo?

Todas as APIs mencionadas oferecem memoização, mas a diferença está no que elas pretendem memoizar, quem pode acessar o cache e quando seu cache é invalidado.

useMemo

Em geral, você deve usar useMemo para armazenar em cache uma computação cara em um Componente do Cliente entre renderizações. Como exemplo, para memoizar uma transformação de dados dentro de um componente.

'use client';

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

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

Neste exemplo, App renderiza dois WeatherReports com o mesmo registro. Embora ambos os componentes façam o mesmo trabalho, eles não podem compartilhar o trabalho. O cache de useMemo é apenas local ao componente.

No entanto, useMemo garante que se App re-renderizar e o objeto record não mudar, cada instância do componente pulará o trabalho e usará o valor memoizado de avgTemp. useMemo só armazenará em cache a última computação de avgTemp com as dependências dadas.

cache

Em geral, você deve usar cache em Componentes do Servidor para memoizar trabalho que pode ser compartilhado entre componentes.

const cachedFetchReport = cache(fetchReport);

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

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

Reescrevendo o exemplo anterior para usar cache, neste caso a segunda instância de WeatherReport poderá pular trabalho duplicado e ler do mesmo cache que a primeira WeatherReport. Outra diferença em relação ao exemplo anterior é que cache também é recomendado para memoizar buscas de dados, ao contrário de useMemo, que só deve ser usado para computações.

No momento, cache deve ser usado apenas em Componentes do Servidor e o cache será invalidado entre solicitações do servidor.

memo

Você deve usar memo para evitar que um componente re-renderize se suas props não mudaram.

'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} />
</>
);
}

Neste exemplo, ambos os componentes MemoWeatherReport chamarão calculateAvg quando forem renderizados pela primeira vez. No entanto, se App re-renderizar, sem alterações no record, nenhuma das props mudará e MemoWeatherReport não re-renderizará.

Comparado a useMemo, memo memoiza a renderização do componente com base nas props em vez de computações específicas. Semelhante a useMemo, o componente memoizado armazena em cache apenas a última renderização com os últimos valores de props. Assim que as props mudam, o cache é invalidado e o componente re-renderiza.


Solução de Problemas

Minha função memoizada ainda é executada mesmo que eu a tenha chamado com os mesmos argumentos

Veja as armadilhas mencionadas anteriormente

Se nenhuma das acima se aplicar, pode ser um problema com como o React verifica se algo existe em cache.

Se seus argumentos não forem primitivos (ex. objetos, funções, arrays), certifique-se de passar a mesma referência de objeto.

Ao chamar uma função memoizada, o React procurará os argumentos de entrada para ver se um resultado já está em cache. O React usará igualdade rasa dos argumentos para determinar se há um acerto de cache.

import {cache} from 'react';

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

function MapMarker(props) {
// 🚩 Errado: props é um objeto que muda a cada renderização.
const length = calculateNorm(props);
// ...
}

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

Neste caso, os dois MapMarkers parecem estar fazendo o mesmo trabalho e chamando calculateNorm com o mesmo valor de {x: 10, y: 10, z:10}. Embora os objetos contenham os mesmos valores, eles não são a mesma referência de objeto, pois cada componente cria seu próprio objeto props.

O React chamará Object.is na entrada para verificar se há um acerto de cache.

import {cache} from 'react';

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

function MapMarker(props) {
// ✅ Bom: Passe primitivos para a função memoizada
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} />
</>
);
}

Uma maneira de resolver isso pode ser passar as dimensões do vetor para calculateNorm. Isso funciona porque as dimensões são primitivos.

Outra solução pode ser passar o objeto vetor em si como um prop para o componente. Precisaremos passar o mesmo objeto para ambas as instâncias do componente.

import {cache} from 'react';

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

function MapMarker(props) {
// ✅ Bom: Passe o mesmo objeto `vector`
const length = calculateNorm(props.vector);
// ...
}

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