Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Zustand con selectores - los componentes solo re-renderizarán cuando su slice cambie
import { create } from "zustand";
interface AppStore {
user: { name: string; role: string } | null;
cart: CartItem[];
notifications: Notification[];
addToCart: (item: CartItem) => void;
clearNotifications: () => void;
}
const useAppStore = create<AppStore>((set) => ({
user: null,
cart: [],
notifications: [],
addToCart: (item) => set((s) => ({ cart: [...s.cart, item] })),
clearNotifications: () => set({ notifications: [] }),
}));
// BUENO: Selector - solo re-renderiza cuando cart cambia
function CartBadge() {
const cartCount = useAppStore((s) => s.cart.length);
return <span>{cartCount}</span>;
}
// MALO: Sin selector - re-renderiza en CUALQUIER cambio del almacén
function CartBadgeBad() {
const store = useAppStore(); // ¡Se suscribe a todo el almacén!
return <span>{store.cart.length}</span>;
}Cuándo usarlo: Cuando React DevTools muestra componentes re-renderizándose debido a cambios de context o almacén que no están relacionados con lo que muestran. Común en dashboards y aplicaciones con estado global compartido.
// ---- ANTES: Suscripción a almacén completo - 47 re-renderizados innecesarios por interacción ----
import { create } from "zustand";
interface DashboardStore {
user: { name: string; avatar: string; role: string };
theme: "light" | "dark";
sidebarOpen: boolean;
notifications: Notification[];
activeTab: string;
searchQuery: string;
filters: Record<string, string>;
cart: CartItem[];
setSidebarOpen: (open: boolean) => void;
setActiveTab: (tab: string) => void;
setSearchQuery: (query: string) => void;
addNotification: (n: Notification) => void;
addToCart: (item: CartItem) => void;
}
const useDashboardStore = create<DashboardStore>((set) => ({
user: { name: "Alice", avatar: "/alice.jpg", role: "admin" },
theme: "light",
sidebarOpen: true,
notifications: [],
activeTab: "overview",
searchQuery: "",
filters: {},
cart: [],
setSidebarOpen: (open) => set({ sidebarOpen: open }),
setActiveTab: (tab) => set({ activeTab: tab }),
setSearchQuery: (query) => set({ searchQuery: query }),
addNotification: (n) => set((s) => ({ notifications: [...s.notifications, n] })),
addToCart: (item) => set((s) => ({ cart: [...s.cart, item] })),
}));
// Cada componente se suscribe al almacén completo
function Header() {
const store = useDashboardStore(); // Re-renderiza al cambiar sidebar, búsqueda, pestaña, etc.
return <header>{store.user.name} ({store.notifications.length})</header>;
}
function Sidebar() {
const store = useDashboardStore(); // Re-renderiza en búsqueda, notificaciones, carrito, etc.
return <nav className={store.sidebarOpen ? "w-64" : "w-16"}>{/* ... */}</nav>;
}
function SearchBar() {
const store = useDashboardStore(); // Re-renderiza en sidebar, notificaciones, carrito, etc.
return (
<input
value={store.searchQuery}
onChange={(e) => store.setSearchQuery(e.target.value)}
/>
);
}
function TabPanel() {
const store = useDashboardStore(); // Re-renderiza en todo
return <div>Active: {store.activeTab}</div>;
}
function CartIcon() {
const store = useDashboardStore(); // Re-renderiza en todo
return <span>Cart: {store.cart.length}</span>;
}
// Escribir en SearchBar re-renderiza todos los 5 componentes = 47 re-renderizados para 10 pulsaciones
// ---- DESPUÉS: Selectores - cada componente se re-renderiza solo para sus propios datos ----
// Misma definición de almacén, pero con selectores
function Header() {
const userName = useDashboardStore((s) => s.user.name);
const notificationCount = useDashboardStore((s) => s.notifications.length);
// Solo re-renderiza cuando user.name o notifications.length cambia
return <header>{userName} ({notificationCount})</header>;
}
function Sidebar() {
const sidebarOpen = useDashboardStore((s) => s.sidebarOpen);
const setSidebarOpen = useDashboardStore((s) => s.setSidebarOpen);
// Solo re-renderiza cuando sidebarOpen cambia
return (
<nav className={sidebarOpen ? "w-64" : "w-16"}>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
</nav>
);
}
function SearchBar() {
const searchQuery = useDashboardStore((s) => s.searchQuery);
const setSearchQuery = useDashboardStore((s) => s.setSearchQuery);
// Solo re-renderiza cuando searchQuery cambia
return (
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
);
}
function TabPanel() {
const activeTab = useDashboardStore((s) => s.activeTab);
// Solo re-renderiza cuando activeTab cambia
return <div>Active: {activeTab}</div>;
}
function CartIcon() {
const cartCount = useDashboardStore((s) => s.cart.length);
// Solo re-renderiza cuando cart.length cambia
return <span>Cart: {cartCount}</span>;
}
// Escribir en SearchBar re-renderiza SOLO SearchBar = 10 re-renderizados para 10 pulsaciones
// Reducción: de 47 re-renderizados a 10 (79% menos)Lo que esto demuestra:
setSidebarOpen, setSearchQuery) son referencias estables - no hay necesidad de useCallbackObject.is para comparar el valor seleccionado entre actualizaciones del almacén. Si el selector devuelve el mismo valor, el componente no se re-renderiza. Esto es fundamentalmente diferente de React Context, que re-renderiza a todos los consumidores en cualquier cambio.filteredItems junto a items y filter crea un problema de sincronización y duplica la superficie de actualización de estado.useSearchParams mantiene el estado de filtro y paginación en la URL. Esto es inherentemente no reactivo (sin re-renderizados hasta la navegación), compartible vía URL, y persiste entre actualizaciones de página.useReducer consolida transiciones de estado relacionadas en una única función de dispatch. Esto previene combinaciones de estado imposibles y reduce el número de setters de estado pasados como props.Patrón de división de context:
// ANTES: Context único - todos los consumidores se re-renderizarán en cualquier cambio
const AppContext = createContext<{
state: AppState;
dispatch: Dispatch<Action>;
} | null>(null);
// DESPUÉS: Contexts divididos - los consumidores de dispatch nunca se re-renderizarán en cambios de estado
const StateContext = createContext<AppState | null>(null);
const DispatchContext = createContext<Dispatch<Action> | null>(null);
function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
}
// Este componente solo hace dispatch - nunca se re-renderiza en cambios de estado
function AddButton() {
const dispatch = useContext(DispatchContext)!;
return <button onClick={() => dispatch({ type: "ADD" })}>Add</button>;
}
// Este componente solo lee count - se re-renderiza solo cuando count cambia
// (aún se re-renderiza en CUALQUIER cambio de estado - usa useMemo para mayor optimización)
function Counter() {
const state = useContext(StateContext)!;
return <span>{state.count}</span>;
}Estado derivado en lugar de estado almacenado:
// MALO: Almacenar estado derivado - debe mantener filteredProducts sincronizado
const [products, setProducts] = useState<Product[]>([]);
const [filter, setFilter] = useState("all");
const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
useEffect(() => {
setFilteredProducts(products.filter((p) => filter === "all" || p.category === filter));
}, [products, filter]);
// BUENO: Calcular en el renderizado - siempre sincronizado, sin effect necesario
const [products, setProducts] = useState<Product[]>([]);
const [filter, setFilter] = useState("all");
const filteredProducts = useMemo(
() => products.filter((p) => filter === "all" || p.category === filter),
[products, filter]
);Estado URL para filtros y paginación:
"use client";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
function ProductFilters() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const category = searchParams.get("category") || "all";
const sort = searchParams.get("sort") || "name";
const page = parseInt(searchParams.get("page") || "1");
function updateParam(key: string, value: string) {
const params = new URLSearchParams(searchParams);
params.set(key, value);
params.set("page", "1"); // Reinicia página al cambiar filtro
router.push(`${pathname}?${params.toString()}`);
}
return (
<div className="flex gap-4">
<select value={category} onChange={(e) => updateParam("category", e.target.value)}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
<select value={sort} onChange={(e) => updateParam("sort", e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</div>
);
}useStore((s) => s.field) infiere el tipo de retorno de la interfaz del almacén.useShallow desde zustand/react/shallow para comparar selecciones de objetos: useStore(useShallow((s) => ({ a: s.a, b: s.b }))).useReducer infiere tipos de acción de la función reductora. Usa una unión discriminada para el tipo de acción.Seleccionar objetos sin useShallow - useStore((s) => ({ a: s.a, b: s.b })) crea un nuevo objeto en cada actualización del almacén, derrotando la optimización del selector. Fix: Usa useShallow desde zustand/react/shallow para comparar propiedades de objetos superficialmente.
Selectores inline que crean nuevas referencias - useStore((s) => s.items.filter(...)) crea un nuevo array en cada actualización del almacén, incluso si el resultado del filtro es idéntico. Fix: O memoiza el selector fuera del componente o usa useMemo dentro del componente.
Context con demasiados valores - Un único context proporcionando { user, theme, locale, cart, notifications } re-renderiza a todos los consumidores cuando cualquier valor cambia. Fix: Divide en contextos separados por dominio, o cambia a Zustand con selectores.
Almacenar estado derivado - Mantener filteredItems en estado junto a items y filter requiere sincronización manual y causa bugs de doble renderizado. Fix: Calcula estado derivado con useMemo en cada renderizado. Siempre está sincronizado y a menudo es más barato que la alternativa basada en efectos.
Usar useState para estado complejo - Múltiples llamadas useState relacionadas (loading, error, data) pueden desincronizarse. Fix: Usa useReducer para estado que tiene múltiples sub-valores o transiciones complejas.
Prop drilling vs rendimiento de context - El prop drilling no causa re-renderizados extra (solo el componente objetivo se re-renderiza). Context causa re-renderizados en todos los consumidores. Fix: Si solo hay 2-3 niveles de profundidad, prop drilling es más performante que context.
| Enfoque | Compensación |
|---|---|
| Zustand con selectores | Suscripciones granulares; dependencia externa |
| React Context dividido | Sin dependencia; setup verboso, aún re-renderiza a todos los consumidores de estado |
| Jotai | Modelo de estado atómico; modelo mental diferente de Zustand |
| Valtio | Basado en proxy; rastreo automático; menos explícito |
| Estado URL | Compartible, persistente; solo para estado de filtro/paginación serializable |
| React Compiler | Auto-optimiza consumidores de context; experimental |
| Prop drilling | Cero overhead; impracticable para componentes profundamente anidados |
Object.is para comparar el valor seleccionado entre actualizaciones del almacén.// MALO: re-renderiza en CUALQUIER cambio del almacén (sidebar, búsqueda, carrito, etc.)
const store = useAppStore();
return <span>{store.cart.length}</span>;
// BUENO: re-renderiza solo cuando cart.length cambia
const cartCount = useAppStore((s) => s.cart.length);
return <span>{cartCount}</span>;filteredProducts junto a products y filter) requiere sincronización manual vía useEffect.useMemo calcula en el renderizado -- siempre sincronizado, sin effect necesario.useStore((s) => ({ a: s.a, b: s.b })) crea una nueva referencia de objeto en cada actualización del almacén.Object.is ve una referencia diferente cada vez e dispara un re-renderizado.useShallow desde zustand/react/shallow para selecciones de objetos.import { useShallow } from "zustand/react/shallow";
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));useState relacionadas (loading, error, data) pueden desincronizarse durante actualizaciones.error y olvidar limpiar loading produce estados imposibles.useReducer para estado con múltiples sub-valores o transiciones complejas.useStore((s) => s.field) infiere el tipo de retorno de la interfaz del almacén automáticamente.useShallow que preserva el tipo de objeto inferido.interface AppStore { cart: CartItem[]; user: User | null; }
// cartCount se infiere como number
const cartCount = useAppStore((s) => s.cart.length);type Action =
| { type: "SET_LOADING"; payload: boolean }
| { type: "SET_DATA"; payload: Product[] }
| { type: "SET_ERROR"; payload: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "SET_LOADING": return { ...state, loading: action.payload };
case "SET_DATA": return { ...state, data: action.payload, loading: false };
case "SET_ERROR": return { ...state, error: action.payload, loading: false };
}
}"use client";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
function Filters() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const category = searchParams.get("category") || "all";
function updateFilter(key: string, value: string) {
const params = new URLSearchParams(searchParams);
params.set(key, value);
router.push(`${pathname}?${params.toString()}`);
}
}Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥