Saltar al contenido principal

Redux en el proyecto

En la lección anterior exploramos los conceptos fundamentales de Redux Toolkit: stores, slices, reducers, selectors, actions y thunks. Sin embargo, el desafío de ingeniería más importante no es memorizar la API de Redux, sino integrarlo dentro de una aplicación empresarial moderna con Next.js (App Router) sin degradar la arquitectura por capas que construimos previamente.

En esta guía paso a paso aprenderás a transformar el estado local de nuestra aplicación de rutinas de entrenamiento en un estado global robusto y predecible, sin alterar una sola línea de código JSX en los componentes visuales.


1. Punto de Partida y Objetivo​

El proyecto del curso dispone de dos ramas de trabajo en GitHub que ilustran esta transición:

El Objetivo de Diseño​

En muchas implementaciones ingenuas, introducir Redux implica llenar los componentes de la interfaz (RoutineForm, RoutineCard, páginas) con llamadas directas a useDispatch(), useSelector() y acciones como dispatch(fetchRoutines()). Esto acopla la vista a la librería de estado y rompe el principio de sustitución.

Nuestro objetivo en esta guía es reemplazar el motor de estado interno preservando intacta la API pública del Hook Fachada (useRoutines). Los componentes de React seguirán invocando createRoutine() y leyendo routines, isLoading y errorMessage sin enterarse jamás de si por debajo los datos provienen de un useState, de Redux Toolkit o de Zustand.


2. Dónde Vive Redux en la Clean Architecture​

Para mantener la integridad arquitectónica, debemos definir con precisión los límites de cada elemento:

  1. Capa de Dominio (domain): Es 100% agnóstica de React, Next.js y Redux. Contiene las Entidades (Routine), las interfaces de repositorio (RoutineRepository) y los Casos de Uso (GetRoutinesUseCase, CreateRoutineUseCase). No importa qué gestor de estado se instale; el Dominio nunca cambia.
  2. Capa de Infraestructura (infrastructure): Implementa los contratos de acceso a datos (RoutineRepositoryImpl) y los datasources (RoutineMockDatasource, API HTTP).
  3. Capa de Presentación (presentation): Aquí reside Redux Toolkit. Redux es un gestor de estado reactivo global para la interfaz de usuario.
    • Los Async Thunks orquestan la asincronía invocando a los Casos de Uso a través del contenedor de inyección de dependencias (routineContainer).
    • El Slice almacena ViewModels (estructuras de datos planas y serializables preparadas para pintar en pantalla), nunca entidades de clase con métodos de negocio.
    • El Hook Fachada (useRoutines) actúa como aduana entre los componentes visuales y el Store de Redux.

Explora el recorrido completo de una mutación en el siguiente diagrama interactivo por capas:

Redux Toolkit en Clean Architecture

3. Estructura de Carpetas: Antes y Después​

La organización física de los archivos refleja la distinción entre lo que es transversal a la aplicación y lo que le pertenece a una funcionalidad (feature) específica.

Estructura de Carpetas con Redux Toolkit en Clean ArchitectureEstructura de Carpetas con Redux Toolkit en Clean Architecture

¿Por qué src/store/ es transversal y el Slice vive en la Feature?​

  • src/store/ (Transversal): Contiene la configuración central del Store (store.ts), los hooks tipados de conveniencia (hooks.ts) y el componente proveedor del contexto (StoreProvider.tsx). Estos elementos aplican a toda la aplicación y unifican todos los reducers de los diferentes módulos.
  • src/features/routines/presentation/store/ (Específico de la Feature): El slice (routines.slice.ts) le pertenece exclusivamente a la funcionalidad de rutinas. Si en el futuro eliminamos el módulo de rutinas, eliminamos su carpeta sin dejar código huérfano en el resto del proyecto.

Comparativa de Archivos en el Repositorio​

compunet3-202602/
├── app/
│ ├── layout.tsx # [MODIFICADO] Envoltorio con <StoreProvider>
│ └── routines-clean/
│ └── page.tsx # [INTACTO] 0 cambios de JSX
├── package.json # [MODIFICADO] Dependencias @reduxjs/toolkit y react-redux
└── src/
├── features/
│ └── routines/
│ ├── di/
│ │ └── routine.container.ts # [INTACTO] Composition Root agnóstico
│ ├── domain/ # [INTACTO] Entidades y Casos de Uso
│ ├── infrastructure/ # [INTACTO] Repositorios y Datasources
│ └── presentation/
│ ├── components/ # [INTACTO] RoutineCard y RoutineForm
│ ├── hooks/
│ │ └── use-routines.ts # [MODIFICADO] Fachada conectada a Redux
│ ├── store/
│ │ └── routines.slice.ts # [NUEVO] Slice, Thunks y Reducers
│ └── viewmodels/
│ └── routine.viewmodel.ts # [INTACTO] DTOs de interfaz y Mappers
└── store/ # [NUEVO] Infraestructura global de Redux
├── hooks.ts # [NUEVO] useAppDispatch y useAppSelector
├── store.ts # [NUEVO] Fábrica makeStore y tipos de TypeScript
└── StoreProvider.tsx # [NUEVO] Provider de cliente ('use client')

4. Guía Paso a Paso de Configuración​

1

Instalar Dependencias Oficiales de Redux

En Next.js requerimos dos librerías: @reduxjs/toolkit (la herramienta oficial recomendada para la lógica del store) y react-redux (las ataduras para conectar el store a los componentes de React).

Ejecuta el siguiente comando en la raíz de tu proyecto:

npm install @reduxjs/toolkit react-redux

Verifica que tu archivo package.json incluya ambas dependencias:

package.json
{
"dependencies": {
"@reduxjs/toolkit": "^2.13.0",
"next": "16.3.8",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-redux": "^9.3.0"
}
}
2

Crear el Slice de la Feature (routines.slice.ts)

El slice encapsula la definición del estado de las rutinas, los thunks asíncronos que orquestan las operaciones con Clean Architecture y los reducers que mutan el estado de forma inmutable.

Crea el archivo src/features/routines/presentation/store/routines.slice.ts:

src/features/routines/presentation/store/routines.slice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { routineContainer } from '../../di/routine.container';
import { CreateRoutineParams } from '../../domain/repositories/routine.repository';
import { RoutineItemViewModel, RoutineViewModelMapper } from '../viewmodels/routine.viewmodel';

interface RoutinesState {
items: RoutineItemViewModel[];
isLoading: boolean;
isMutating: boolean;
errorMessage: string | null;
}

const initialState: RoutinesState = {
items: [],
isLoading: false,
isMutating: false,
errorMessage: null,
};

/**
* 1. Thunk de Consulta / Lectura (Query)
* Invoca el caso de uso GetRoutinesUseCase y mapea las entidades de dominio a ViewModels.
*/
export const fetchRoutines = createAsyncThunk(
'routines/fetchAll',
async (_, { rejectWithValue }) => {
try {
const domainEntities = await routineContainer.getRoutines.execute();
return domainEntities.map((entity) => RoutineViewModelMapper.toViewModel(entity));
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Error al cargar rutinas';
return rejectWithValue(message);
}
}
);

/**
* 2. Thunk de Mutación (Mutation)
* Invoca el caso de uso CreateRoutineUseCase para guardar una nueva rutina y retorna el nuevo ViewModel.
*/
export const createRoutineMutation = createAsyncThunk(
'routines/create',
async (params: CreateRoutineParams, { rejectWithValue }) => {
try {
const newEntity = await routineContainer.createRoutine.execute(params);
return RoutineViewModelMapper.toViewModel(newEntity);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'No se pudo crear la rutina';
return rejectWithValue(message);
}
}
);

export const routinesSlice = createSlice({
name: 'routines',
initialState,
reducers: {
clearError: (state) => {
state.errorMessage = null;
},
},
extraReducers: (builder) => {
// --- Ciclo de vida de Lectura (fetchRoutines) ---
builder
.addCase(fetchRoutines.pending, (state) => {
state.isLoading = true;
state.errorMessage = null;
})
.addCase(fetchRoutines.fulfilled, (state, action: PayloadAction<RoutineItemViewModel[]>) => {
state.isLoading = false;
state.items = action.payload;
})
.addCase(fetchRoutines.rejected, (state, action) => {
state.isLoading = false;
state.errorMessage = (action.payload as string) ?? 'Error al obtener rutinas';
});

// --- Ciclo de vida de Mutación (createRoutineMutation) ---
builder
.addCase(createRoutineMutation.pending, (state) => {
state.isMutating = true;
state.errorMessage = null;
})
.addCase(createRoutineMutation.fulfilled, (state, action: PayloadAction<RoutineItemViewModel>) => {
state.isMutating = false;
state.items.unshift(action.payload); // Actualización en el estado local de Redux
})
.addCase(createRoutineMutation.rejected, (state, action) => {
state.isMutating = false;
state.errorMessage = (action.payload as string) ?? 'Error al guardar rutina';
});
},
});

export const { clearError } = routinesSlice.actions;
export const routinesReducer = routinesSlice.reducer;
Análisis Detallado de Diseño en el Slice
  1. ¿Por qué el estado almacena RoutineItemViewModel y no Routine (Entidad)?:
    Las entidades de dominio pueden contener métodos de negocio, constructores privados o propiedades complejas no serializables. Redux exige estrictamente que el estado sea un objeto plano de JavaScript serializable. Además, el RoutineItemViewModel ya viene con los textos formateados y los colores calculados listos para ser consumidos de inmediato por la UI.
  2. ¿Para qué sirve rejectWithValue(message)?:
    Si dejamos que el bloque catch lance un error convencional con throw err, Redux Toolkit captura el error y coloca un objeto genérico serializado en action.error. Al utilizar rejectWithValue(message), el mensaje de error personalizado viaja directamente en action.payload cuando la acción llega al estado rejected, simplificando enormemente su lectura en el reducer.
  3. reducers frente a extraReducers:
    • reducers: Define acciones síncronas generadas internamente por el propio slice (como clearError), generando automáticamente creadores de acción en routinesSlice.actions.
    • extraReducers: Escucha acciones generadas externamente al slice, como el ciclo de vida de promesas generado por createAsyncThunk (pending, fulfilled, rejected).
  4. Actualización Pesimista frente a Optimista:
    El proyecto implementa un flujo pesimista: la inserción en el array (state.items.unshift(action.payload)) ocurre únicamente en el caso fulfilled, cuando el datasource y la base de datos confirmaron que el registro fue guardado. Si quisiéramos un flujo optimista (estilo chat), insertaríamos un elemento temporal en pending y revertiríamos la inserción mediante un rollback en rejected.
3

Crear la Fábrica del Store (store.ts)

En arquitecturas de servidor como Next.js App Router, la configuración del Store requiere una fábrica (makeStore) en lugar de exportar una instancia global (singleton).

Crea el archivo src/store/store.ts:

src/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { routinesReducer } from '@/src/features/routines/presentation/store/routines.slice';

/**
* Fábrica para crear una nueva instancia del Store de Redux.
*
* ¿Por qué una función en vez de una constante global?
* En Next.js (App Router con Server Components / SSR), una constante global
* compartiría el mismo store entre múltiples peticiones de usuarios en el servidor.
* Con makeStore(), cada contexto obtiene su propia instancia limpia.
*/
export const makeStore = () => {
return configureStore({
reducer: {
routines: routinesReducer,
},
});
};

// Tipos inferidos a partir del propio Store
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];
El Peligro del Store Singleton en SSR: Fuga de Datos (State Pollution)

En SPAs clásicas (Vite, CRA) se suele hacer: export const store = configureStore(...).
En Next.js con Server Components, el código del módulo vive en el proceso compartido de Node.js en el servidor. Si dos usuarios concurrentes (Usuario A y Usuario B) solicitan páginas al mismo tiempo, un Store Singleton compartiría la misma memoria RAM entre ambos clientes. Si el servidor inyectara datos del Usuario A en el store, el Usuario B podría recibir el HTML con la información privada de A.

La solución oficial es definir la fábrica makeStore = () => configureStore(...). Cada ciclo de petición en el servidor o cliente genera su propia instancia aislada.

Compara ambos comportamientos en la siguiente pieza interactiva:

makeStore frente a Singleton en Next.js
4

Crear los Hooks Tipados (hooks.ts)

Los hooks nativos de React-Redux (useDispatch y useSelector) son genéricos y no conocen los tipos de nuestro estado global ni los tipos de los Thunks asíncronos. Para evitar tener que escribir useSelector((state: RootState) => ...) en cada archivo, creamos hooks fuertemente tipados.

Crea el archivo src/store/hooks.ts:

src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector, useStore } from 'react-redux';
import type { AppDispatch, AppStore, RootState } from './store';

// Hooks tipados para usar en toda la aplicación en lugar de los hooks genéricos de react-redux (`useDispatch` y `useSelector`).
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppStore: () => AppStore = useStore;
Nota sobre Redux Toolkit 2.0+ y .withTypes

En versiones recientes de Redux Toolkit (v2.0+) y React-Redux (v9.0+), se introdujo el método de conveniencia .withTypes:

Guía Paso a Paso de Configuración
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

En la rama del curso se utilizó la anotación de tipos explícita tradicional (TypedUseSelectorHook<RootState>). Ambos enfoques son técnicamente equivalentes en tiempo de compilación y garantizan autocompletado total e inferencia estricta de tipos.

5

Crear el Proveedor de Cliente (StoreProvider.tsx)

react-redux requiere el Context API de React para propagar el Store a los componentes descendientes. Dado que el Context API solo existe en el navegador (Client Components), debemos encapsular <Provider> en un componente con la directiva 'use client'.

Crea el archivo src/store/StoreProvider.tsx:

src/store/StoreProvider.tsx
'use client';

import { useState, type ReactNode } from 'react';
import { Provider } from 'react-redux';
import { makeStore, AppStore } from './store';

/**
* Proveedor de Redux para Next.js App Router.
*
* ¿Por qué 'use client'?
* Redux utiliza el React Context API internamente, lo cual solo está disponible
* en Client Components.
* ¿Por qué useState(makeStore)?
* Garantiza que la instancia del store se cree exactamente una vez durante el ciclo
* de vida del cliente y permanezca estable sin reinicializarse en cada render.
*/
export function StoreProvider({ children }: { children: ReactNode }) {
const [store] = useState<AppStore>(makeStore);

return <Provider store={store}>{children}</Provider>;
}
Garantía de Instancia Única con useState

Si hiciéramos const store = makeStore() dentro del cuerpo del componente, cada vez que el componente padre re-renderice se crearía un nuevo store de Redux, destruyendo todo el estado acumulado del usuario en la sesión.
Al pasar la función makeStore como argumento perezoso a useState(makeStore), React garantiza que la fábrica solo se invoca en el montaje inicial del cliente y preserva la misma instancia en memoria para siempre.

(En la documentación oficial de Redux para Next.js también se suele ver el patrón con useRef<AppStore | null>(null) y if (!storeRef.current) storeRef.current = makeStore(). Ambas alternativas cumplen el mismo propósito pedagógico).

6

Envolver la Aplicación en app/layout.tsx

Ahora debemos conectar el StoreProvider en el punto de entrada raíz de la aplicación para que todas las páginas y componentes tengan acceso al Store.

Modifica app/layout.tsx:

app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import { StoreProvider } from "@/src/store/StoreProvider";

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`h-full antialiased`}
>
<body className="min-h-full flex flex-col">
<StoreProvider>
{children}
</StoreProvider>
</body>
</html>
);
}
Arquitectura de Interleaving: El Layout Sigue Siendo Server Component

Observa un detalle fundamental de la arquitectura de Next.js: el archivo app/layout.tsx NO lleva 'use client'.
Sigue siendo un Server Component. StoreProvider es una hoja de cliente que recibe a children como propiedad.
Gracias al patrón de Interleaving (estudiado en la semana 9), Next.js puede renderizar componentes de servidor dentro de {children} y pasarlos ya resueltos a través del proveedor de Redux sin convertir toda la aplicación en una SPA tradicional.

7

Refactorizar el Hook Fachada (use-routines.ts)

Llegamos al paso crucial: sustituir la implementación basada en useState por Redux Toolkit en el Hook Fachada, manteniendo idéntica la firma de retorno.

Observa la comparación entre ambas implementaciones:

src/features/routines/presentation/hooks/use-routines.ts
'use client';

import { useEffect, useCallback } from 'react';
import { useAppDispatch, useAppSelector } from '@/src/store/hooks';
import { fetchRoutines, createRoutineMutation } from '../store/routines.slice';
import { CreateRoutineParams } from '../../domain/repositories/routine.repository';

/**
* Hook fachada (Presentation Layer).
*
* Actúa como adaptador entre los componentes de React y el Store de Redux:
* 1. Oculta los detalles de Redux a los componentes visuales (RoutineForm, RoutineCard).
* 2. Mantiene exactamente la misma API pública que tenía con useState.
*/
export function useRoutines() {
const dispatch = useAppDispatch();

// 1. Lectura del estado global gestionado por Redux
const routines = useAppSelector((state) => state.routines.items);
const isLoading = useAppSelector((state) => state.routines.isLoading);
const isMutating = useAppSelector((state) => state.routines.isMutating);
const errorMessage = useAppSelector((state) => state.routines.errorMessage);

// 2. Carga inicial: si el store aún no tiene datos y no está cargando, solicitamos las rutinas
useEffect(() => {
if (routines.length === 0 && !isLoading) {
dispatch(fetchRoutines());
}
}, [dispatch, routines.length, isLoading]);

// 3. Mutación de creación: delega al thunk asíncrono
const createRoutine = async (params: CreateRoutineParams): Promise<boolean> => {
const resultAction = await dispatch(createRoutineMutation(params));
// Devuelve true si la acción culminó en fulfilled (éxito), o false si fue rejected (error)
return createRoutineMutation.fulfilled.match(resultAction);
};

// 4. Recarga manual
const reload = useCallback(() => {
dispatch(fetchRoutines());
}, [dispatch]);

return {
routines,
// La UI considera que está cargando si estamos en consulta inicial o en proceso de guardado
isLoading: isLoading || isMutating,
errorMessage,
reload,
createRoutine,
};
}
Claves de la Refactorización del Hook
  1. Carga Inicial Condicionada:
    if (routines.length === 0 && !isLoading) dispatch(fetchRoutines());
    A diferencia del useState original (que forzaba una consulta a red cada vez que el componente se montaba), con Redux los datos persisten en la memoria global. Si el usuario navega a otra pantalla y regresa, routines.length > 0, por lo que no se repite la petición a la API, obteniendo una navegación instantánea.
  2. Desempaquetado del Thunk con .fulfilled.match(...):
    Cuando despachas un thunk con dispatch(createRoutineMutation(params)), la promesa que retorna dispatch siempre resuelve (incluso si la operación interna falló y se fue por rejected). Para determinar si la operación tuvo éxito sin envolver todo en otro try/catch, Redux Toolkit proporciona el type guard createRoutineMutation.fulfilled.match(resultAction), el cual retorna true si la acción fue exitosa o false si falló. Esto permite mantener la firma de retorno Promise<boolean> esperada por el formulario de la UI.
  3. Estado de Carga Unificado:
    isLoading: isLoading || isMutating. La vista solo necesita saber si hay una operación en curso para deshabilitar botones o mostrar indicadores de carga (spinners).
8

Verificar la Integración y Depurar con Redux DevTools

Abre la página en tu navegador (http://localhost:3000/routines-clean). Comprobarás que los componentes visuales:

  • RoutineCard: Sigue recibiendo su ViewModel por props y dibuja las tarjetas exactamente igual.
  • RoutineForm: Sigue invocando createRoutine(values) y reiniciando el formulario cuando retorna true.
  • RoutinesCleanPage: Sigue leyendo las rutinas del hook y mapeándolas a componentes.

Inspección con Redux DevTools​

Instala la extensión Redux DevTools en Chrome o Firefox. Al abrir las herramientas de desarrollador observarás la secuencia completa de acciones despachadas por el motor:

1. @@INIT # Inicialización del Store en el cliente
2. routines/fetchAll/pending # Disparado al montar useRoutines (isLoading = true)
3. routines/fetchAll/fulfilled # Llegaron los ViewModels (items = [...], isLoading = false)
4. routines/create/pending # Usuario pulsó "Guardar rutina" (isMutating = true)
5. routines/create/fulfilled # Datasource confirmó y se insertó el nuevo elemento

Puedes hacer clic en cualquier acción para ver la diferencia de estado (diff), inspeccionar el payload exacto o hacer "viaje en el tiempo" (Time Travel Debugging) para reproducir errores paso a paso.


5. Guía para Aplicarlo en tu Propio Proyecto​

Cuando construyas tu proyecto semestral o agregues una segunda funcionalidad (por ejemplo, gestión de ejercicios exercises o de usuarios users), sigue esta lista de comprobación:

  1. Definir los ViewModels de la nueva feature:
    Crear los tipos planos serializables en src/features/<feature>/presentation/viewmodels/.

  2. Crear el Slice en la Capa de Presentación:
    Crear src/features/<feature>/presentation/store/<feature>.slice.ts:

    • Definir la interfaz <Feature>State almacenando arrays de ViewModels.
    • Crear los Thunks que llamen a los Casos de Uso del container (<feature>Container.<usecase>.execute()).
    • Usar rejectWithValue en el bloque catch.
    • Manejar pending, fulfilled y rejected en extraReducers.
  3. Registrar el nuevo Reducer en la Fábrica Global:
    Abrir src/store/store.ts y añadir la clave correspondiente en makeStore:

src/store/store.ts
export const makeStore = () => configureStore({
reducer: {
routines: routinesReducer,
exercises: exercisesReducer, // <-- Nuevo módulo registrado
},
});
  1. Construir o Adaptar el Hook Fachada:
    Crear src/features/<feature>/presentation/hooks/use-<feature>.ts:

    • Leer los datos con useAppSelector((state) => state.<feature>.items).
    • Despachar las intenciones con useAppDispatch().
    • Preservar la API amigable para los componentes visuales.
  2. Conectar la Vista:
    Consumir únicamente el Hook Fachada en los componentes JSX. No importar react-redux dentro de los componentes visuales.


6. Errores Comunes al Integrar Redux con Clean Architecture​

Error ComúnCausa del ProblemaConsecuencia en la AplicaciónSolución Arquitectónica
Olvidar 'use client' en StoreProvider.tsxLos Server Components no pueden ejecutar React Context ni hooks de React.Error de compilación en Next.js: "createContext only works in Client Components".Declarar la directiva 'use client'; en la primera línea de StoreProvider.tsx.
Exportar un Store Singleton en el ServidorDeclarar export const store = configureStore(...) a nivel de módulo en Next.js.Fuga de datos entre peticiones concurrentes (Cross-Request State Pollution).Usar una función fábrica export const makeStore = () => configureStore(...).
Almacenar Entidades de Clase en el EstadoGuardar instancias con métodos (ej. new Routine(...)) dentro de state.items.Errores de serialización en Redux DevTools y fallos en hidratación SSR.Guardar siempre ViewModels planos generados a través de mappers.
Llamar a fetch() directo dentro del ThunkSaltarse los Casos de Uso y llamar directamente a endpoints HTTP en el thunk.Rompe Clean Architecture; duplica lógica de negocio e impide testear el dominio.El Thunk debe llamar únicamente a los casos de uso expuestos por el Composition Root (routineContainer).
Usar Redux directamente en los Componentes JSXInvocar useAppDispatch o useAppSelector dentro de RoutineCard o RoutineForm.Acopla la interfaz visual a Redux, impidiendo reutilizar componentes o cambiar de gestor de estado.Consumir siempre el Hook Fachada (useRoutines), que encapsula a Redux.
Crear el Store sin inicialización perezosaEscribir const store = makeStore() directamente en el cuerpo de StoreProvider.Cada re-render del layout reinicializa el store, reseteando los datos del usuario.Utilizar el inicializador perezoso de estado: const [store] = useState(makeStore).

Cuestionario de Autoevaluación​

Cargando cuestionario...