useSyncExternalStore
useSyncExternalStore
es un Hook de React que te permite suscribirte a una fuente de almacenamiento de datos (store) externa.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Referencia
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Llama a useSyncExternalStore
en el nivel superior de tu componente para leer un valor de una fuente de almacenamiento de datos externa.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
Devuelve la instantánea de los datos en la fuente de almacenamiento de datos. Necesitas pasar dos funciones como argumentos:
- La función
subscribe
debe suscribirse a la fuente de almacenamiento de datos y devolver una función que cancela dicha suscripción. - La función
getSnapshot
debería obtener una instantánea de los datos de la fuente de almacenamiento de datos.
Parámetros
-
subscribe
: Una función que toma un solo argumentocallback
y lo suscribe a la fuente de almacenamiento de datos. Cuando la fuente de almacenamiento de datos cambia, debe invocar elcallback
proporcionado. Esto hará que el componente se vuelva a rerenderizar. La funciónsubscribe
debería devolver una función que limpia dicha suscripción. -
getSnapshot
: Una función que devuelve una instantánea de los datos de la fuente de almacenamiento de datos que necesita el componente. Si bien la fuente de almacenamiento de datos no ha cambiado, las llamadas repetidas agetSnapshot
deben devolver el mismo valor. Si la fuente de almacenamiento de datos cambia y el valor devuelto es diferente (usando para la comparaciónObject.is
), React volverá a rerenderizar el componente. -
opcional
getServerSnapshot
: Una función que devuelve la instantánea inicial de los datos de la fuente de almacenamiento de datos. Se usará solo durante el renderizado en el servidor y durante la hidratación del contenido renderizado por el servidor en el cliente. La instantánea del servidor debe ser la misma entre el cliente y el servidor, y generalmente se serializa y pasa del servidor al cliente. Si no se proporciona esta función, el renderizado del componente en el servidor generará un error.
Devuelve
La instantánea actual de la fuente de almacenamiento que puedes usar en tu lógica de renderizado.
Advertencias
-
La instantánea de la fuente de almacenamiento de datos devuelta por
getSnapshot
debe ser inmutable. Si la fuente subyacente tiene datos mutables, devuelve una nueva instantánea inmutable si los datos han cambiado. De lo contrario, devuelve la última instantánea almacenada en caché. -
Si se pasa una función
subscribe
diferente durante un rerenderizado, React se volverá a suscribir a la fuente de almacenamiento de datos usando la funciónsubscribe
recién pasada. Puedes evitarlo declarandosubscribe
fuera del componente.
Uso
Suscripción a una fuente de almacenamiento datos externa
Normalmente la mayoría de tus componentes de React solo leerán datos de sus props,, estado, y contexto.. Sin embargo, a veces un componente necesita leer algunos datos de alguna fuente de almacenamiento fuera de React que cambia con el tiempo. Esto incluye:
- Bibliotecas de gestión de estado de terceros que mantienen el estado fuera de React.
- Las API del navegador que exponen un valor mutable y eventos para suscribirse a sus cambios.
Llama a useSyncExternalStore
en el primer nivel de tu componente para leer un valor de una fuente de datos externa.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
Esto devuelve la instantánea del dato en la fuente de almacenamiento de datos. Necesitas pasar dos funciones como argumentos:
- La función
subscribe
deberá suscribirse a la fuente de almacenamiento de datos y devolver una función que permita des suscribirse. - La función
getSnapshot
deberá obtener una instantánea de los datos de la fuente.
React utilizará estas funciones para mantener tu componente suscrito a la fuente de almacenamiento de datos y volver a renderizarlo con los cambios.
Por ejemplo, en el sandbox debajo, todosStore
se implementa como una fuente de almacenamiento de datos externa que almacena datos fuera de React. El componente TodosApp
se conecta a esta fuente de almacenamiento externa de datos con el Hook useSyncExternalStore
.
import { useSyncExternalStore } from 'react'; import { todosStore } from './todoStore.js'; export default function TodosApp() { const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot); return ( <> <button onClick={() => todosStore.addTodo()}>Add todo</button> <hr /> <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); }
Suscripción a una API del navegador
Otra razón para usar useSyncExternalStore
es cuando deseas suscribirte a algún valor expuesto por el navegador que cambia con el tiempo. Por ejemplo, supón que deseas que tu componente muestre si la conexión de red está activa. El navegador expone esta información a través de una propiedad llamada navigator.onLine
.
Este valor puede cambiar con el tiempo sin que React sea notificado, por lo que necesitas leerlo con useSyncExternalStore
.
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
Para implementar la función getSnapshot
, lee el valor actual de la API del navegador:
function getSnapshot() {
return navigator.onLine;
}
A continuación, debes implementar la función subscribe
. Por ejemplo, cuando navigator.onLine
cambia, el navegador activa los eventos online
y offline
en el objeto window
. Debe suscribir el argumento callback
a los eventos correspondientes y luego devolver una función que limpie estas suscripciones:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
Ahora React sabe cómo leer el valor de la API navigator.onLine
externa y cómo suscribirse a sus cambios. Intenta desconectar tu dispositivo de la red y observa que como respuesta el componente se vuelve a renderizar:
import { useSyncExternalStore } from 'react'; export default function ChatIndicator() { const isOnline = useSyncExternalStore(subscribe, getSnapshot); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>; } function getSnapshot() { return navigator.onLine; } function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; }
Extracción de lógica en un Hook personalizado
Por lo general, no deberías escribir useSyncExternalStore
directamente en tus componentes. En su lugar, normalmente lo llamarás desde tu propio Hook personalizado. Esto te permite usar la misma fuente de almacenamiento externa desde diferentes componentes.
Por ejemplo, este Hook personalizado useOnlineStatus
monitoriza si la red está en línea:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}
Ahora diferentes componentes pueden llamar a useOnlineStatus
sin repetir la implementación subyacente:
import { useOnlineStatus } from './useOnlineStatus.js'; function StatusBar() { const isOnline = useOnlineStatus(); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ Progress saved'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'Save progress' : 'Reconnecting...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
Añadir compatibilidad con el renderizado en el servidor
Si tu aplicación React usa renderizado en el servidor,, tus componentes React también se ejecutarán fuera del entorno del navegador para generar el HTML inicial. Esto crea algunos desafíos cuando se conecta a una fuente de datos externa:
- Si te estás conectando a una API única del navegador, no funcionará porque no existe en el servidor.
- Si te estás conectando a una fuente de datos externa de terceros, necesitarás que sus datos coincidan entre el servidor y el cliente.
Para resolver estos problemas, pasa una función getServerSnapshot
como tercer argumento a useSyncExternalStore
:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // Always show "Online" for server-generated HTML
}
function subscribe(callback) {
// ...
}
La función getServerSnapshot
es similar a getSnapshot
, pero solo se ejecuta en dos situaciones:
- Se ejecuta en el servidor al generar el HTML.
- Se ejecuta en el cliente durante la hidratación, es decir, cuando React toma el HTML del servidor y lo hace interactivo.
Esto te permite proporcionar la instantánea del valor inicial que se utilizará antes de que la aplicación se vuelva interactiva. Si no hay un valor inicial significativo para el renderizado en el servidor, puedes forzar el componente para que se renderice solo en el cliente.
Solución de problemas
Recibo un error: »The result of getSnapshot
should be cached (el resultado de getSnapshot
debería almacenarse en caché)»
Si obtienes este error, significa que tu función getSnapshot
devuelve un nuevo objeto cada vez que se llama, por ejemplo:
function getSnapshot() {
// 🔴 Do not return always different objects from getSnapshot
return {
todos: myStore.todos
};
}
React volverá a rerenderizar el componente si el valor de retorno de getSnapshot
es diferente al de la última vez. Por eso, si siempre devuelves un valor diferente, entrarás en un bucle infinito y obtendrás este error.
Tu objeto getSnapshot
solo debería devolver un objeto diferente si algo realmente ha cambiado. Si tu fuente de almacenamiento de datos externa contiene datos inmutables, puede devolver esos datos directamente:
function getSnapshot() {
// ✅ You can return immutable data
return myStore.todos;
}
Si los datos de tu fuente de almacenamiento son mutables, tu función getSnapshot
debería devolver una instantánea inmutable de la misma. Esto significa que sí necesita crear nuevos objetos, pero no debería hacer esto en cada llamada. En su lugar, debe almacenar la última instantánea calculada y devolver la misma instantánea que la última vez si los datos almacenados no han cambiado. La forma en que determina si los datos mutables han cambiado depende de cómo se implemente tu fuente de almacenamiento mutable.
Mi función subscribe
se llama después de cada rerenderizado
Esta función subscribe
se define dentro de un componente, por lo que es diferente en cada rerenderizado:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Siempre una función diferente, por lo que React se volverá a suscribir en cada rerenderizado
function subscribe() {
// ...
}
// ...
}
React se volverá a suscribir a tu fuente de almacenamiento si pasas una función de subscribe
diferente entre rerenderizados. Si esto causa problemas de rendimiento y desea evitar volver a suscribirse a la fuente de almacenamiento de datos externa, mueva la función subscribe
fuera:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Siempre la misma función, por lo que React no necesitará volver a suscribirse
function subscribe() {
// ...
}
Alternativamente, puedes envolver subscribe
con useCallback
para solo resuscribirte cuando algún argumento cambie:
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}