Hook: useSyncExternalStore
Introducción
El hook useSyncExternalStore fue introducido en React 18 como una solución estandarizada para suscribirse a almacenes de estado externos (external stores) en aplicaciones React. Es útil para trabajar con bibliotecas como Redux, Zustand o cualquier sistema de estado que gestione el estado fuera del ciclo de vida de React.
Este hook asegura que React mantenga sincronizado su estado con el estado externo en aplicaciones tanto en Cliente como en Servidor.
1. Qué es useSyncExternalStore? #
useSyncExternalStore te permite:
- Suscribirte a un estado externo para recibir actualizaciones.
- Garantizar que el estado se sincroniza correctamente entre renderizados.
- Proveer un estado inicial para renderizado en servidor (SSR).
Sintaxis #
1const state = useSyncExternalStore(suscribe, getSnapshot, getServerSnapshot);
suscribe: Una función que se llama para suscribirse a cambios en el estado externo. Debe devolver una función para desuscribirse.getSnapshot: Devuelve el valor actual del estado externo.getServerSnapshot(opcional): Devuelve el estado inicial para renderizado en servidor.
2. Ejemplo Básico: Contador con Estado Externo #
Imagina un contador gestionado fuera de React.
Almacén Externo #
1// counterStore.js 2let count = 0; 3let listeners = []; 4 5export const increment = () => { 6 count += 1; 7 listeners.forEach((listener) => listener()); 8}; 9 10export const subscribe = (listener) => { 11 listeners.push(listener); 12 return () => { 13 listeners = listeners.filter((l) => l !== listener); 14 }; 15}; 16 17export const getCount = () => count;
Componente React #
1import React from "react"; 2import { useSyncExternalStore } from "react"; 3import { subscribe, getCount, increment } from "./counterStore"; 4 5function Contador() { 6 const count = useSyncExternalStore(subscribe, getCount); 7 8 return ( 9 <div> 10 <h1>Contador: {count}</h1> 11 <button onClick={increment}>Incrementar</button> 12 </div> 13 ); 14} 15 16export default Contador;
Explicación #
subscribe: React se suscribe al almacén externo (counterStore).getCount: Devuelve el valor actual del contador.- Sincronización: El componente se renderiza automáticamente cuando el contador cambia.
3. Ejemplo con Renderizado en Servidor (SSR) #
Cuando renderizas React en servidor, necesitas pasar un estado inicial para evitar inconsistencias.
Almacén Externo con Estado Inicial #
1// themeStore.js 2let theme = "light"; 3let listeners = []; 4 5export const setTheme = (newTheme) => { 6 theme = newTheme; 7 listeners.forEach((listener) => listener()); 8}; 9 10export const subscribe = (listener) => { 11 listeners.push(listener); 12 return () => { 13 listeners = listeners.filter((l) => l !== listener); 14 }; 15}; 16 17export const getTheme = () => theme; 18 19// Para SSR 20export const getServerTheme = () => "light"; // Estado inicial en servidor
Componente React #
1import React from "react"; 2import { useSyncExternalStore } from "react"; 3import { subscribe, getTheme, getServerTheme, setTheme } from "./themeStore"; 4 5function Tema() { 6 const theme = useSyncExternalStore(subscribe, getTheme, getServerTheme); 7 8 return ( 9 <div> 10 <h1>Tema Actual: {theme}</h1> 11 <button onClick={() => setTheme("light")}>Claro</button> 12 <button onClick={() => setTheme("dark")}>Oscuro</button> 13 </div> 14 ); 15} 16 17export default Tema;
Explicación #
getServerTheme: Proporciona un valor inicial del tema para renderizado en servidor.- Sincronización Universal: React sincroniza correctamente el tema entre cliente y servidor.
4. Diferencias con useState y useReducer #
| Característica | useSyncExternalStore | useState / useReducer |
|---|---|---|
| Fuente del Estado | Externo (almacén global o biblioteca) | Interno al componente |
| Sincronización | Garantiza sincronización con React | No aplica, el estado es propio |
| Renderizado en Servidor | Compatible con SSR | No diseñado específicamente para SSR |
5. Comparación con useEffect #
Aunque podrías suscribirte a un almacén externo con useEffect, useSyncExternalStore es más seguro y eficiente.
Con useEffect #
1import React, { useState, useEffect } from "react"; 2import { subscribe, getCount } from "./counterStore"; 3 4function Contador() { 5 const [count, setCount] = useState(getCount()); 6 7 useEffect(() => { 8 const unsubscribe = subscribe(() => { 9 setCount(getCount()); 10 }); 11 return unsubscribe; 12 }, []); 13 14 return ( 15 <div> 16 <h1>Contador: {count}</h1> 17 </div> 18 ); 19} 20 21export default Contador;
Con useSyncExternalStore #
1import React from "react"; 2import { useSyncExternalStore } from "react"; 3import { subscribe, getCount } from "./counterStore"; 4 5function Contador() { 6 const count = useSyncExternalStore(subscribe, getCount); 7 8 return ( 9 <div> 10 <h1>Contador: {count}</h1> 11 </div> 12 ); 13} 14 15export default Contador;
Diferencias #
| Aspecto | useEffect | useSyncExternalStore |
|---|---|---|
| Código más limpio | No | Sí |
| Renderizado en Servidor (SSR) | No soportado | Soportado |
| Eficiencia | Menos eficiente | Más eficiente |
6. Ventajas de useSyncExternalStore #
-
Integración estándar:
- Trabaja perfectamente con React Concurrent Mode y SSR.
-
Simplifica código:
- Proporciona una solución directa para manejar suscripciones sin usar
useEffect.
- Proporciona una solución directa para manejar suscripciones sin usar
-
Evita Renderizados Innecesarios:
- Solo actualiza el componente cuando el estado externo cambia.
7. Limitaciones #
-
Solo para Estados Externos:
- No reemplaza
useStateouseReducerpara manejar estado interno del componente.
- No reemplaza
-
Mayor Configuración Inicial:
- Requiere definir funciones como
subscribeygetSnapshot.
- Requiere definir funciones como
-
Complejidad para Nuevos Desarrolladores:
- Puede ser más difícil de entender en comparación con
useStateouseEffect.
- Puede ser más difícil de entender en comparación con
Resumen del tema
Conceptos clave #
- Suscripción a Stores Externos (React 18+):
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)conecta almacenes fuera de React (Zustand, Redux, APIs del navegador). - Prevención de Tearing: garantiza lecturas sincrónicas y consistentes del estado durante el renderizado concurrente (Concurrent Rendering).
- Contrato de Funciones:
subscribe(callback): registra el listener y retorna una función de limpieza/desuscripción.getSnapshot(): devuelve una instantánea inmutable del valor actual del almacén en el cliente.getServerSnapshot(): provee el estado inicial estático durante la hidratación en SSR.
- Sustitución de Patrones con
useEffect: elimina condiciones de carrera y dobles renderizados innecesarios asociados al patrón legacyuseEffect + useState.
Qué debes recordar #
useSyncExternalStore es el estándar recomendado para suscribir componentes a almacenes de estado externos y APIs del navegador de forma segura ante SSR y modo concurrente.