Después de haber entendido algunos conceptos de Redux, llegó el momento de utilizarlo como tal 😁.
Setup
Para hacer el setup de la app usaré vite. Vite es una alternativa a webpack, el cual mejora la experiencia de usuario y es mucho más rápido. Si quieres saber más acerca de los beneficios de vite, puedes visitar el siguiente artículo
Para crear una aplicación con vite y react, solo es necesario abrir tu consola y poner el siguiente comando.
yarn create vite redux-tutorial --template react-ts
La opción de --template
le dice a vite con que template inicializar el proyecto, en este caso el de react-ts
ahora ya se tendrá la siguiente estructura.
Ahora empecemos con las dependencias, como habíamos dicho utilizaremos redux con react, para eso tenemos que instalar el paquete react-redux
, el cual trae lo necesario para hacer la conexión a redux, además a ellos vamos a instalar @reduxjs/toolkit
el cual trae algunos superpoderes para redux
yarn add react-redux @reduxjs/toolkit
¿Que es redux toolkit?
Redux esta bien, pero era un poco complicado. Actualmente, contamos con Redux toolkit él ofrece las siguientes soluciones:
- Simplifica la configuración de redux
- Elimina la necesidad de agregar múltiples paquetes para tener una aplicación escalable.
- Reduce el código repetitivo.
Actualmente no se recomienda usar react-redux
sin @reduxjs/toolkit
.
Preparando el Store
Para empezar a escribir lógica con redux, lo primero que se tiene que hacer es configurar el Store
. Para eso Redux toolkit provee un método que nos ayuda con el procedimiendo, el cual se llama configureStore
.
// store/index.ts
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {},
devTools: process.env.NODE_ENV !== "production",
});
export default store;
Ahora ya tenemos él store :) . Al hacer esto redux-toolkit
ha puesto algunas configuracioens por defecto, las cuales iré comentando conforme avancemos en el ejemplo. En este instante podemos hablar de las devTools las cuales son indispensables para poder depurar la aplicación. En este caso la opción devtools
se activa solo en producción, también puedes personalizar el funcionamiento, pasando un objeto de opciones.
Conexión con React
Ahora es momento de poner disponible el store hacia React, para eso react-redux
provee un Provider
para poner disponible el Store
en todo el árbol de componentes.
import "./App.css";
import { Provider as ReduxProvider } from "react-redux";
import store from "./store";
function App() {
return (
<ReduxProvider store={store}>
<div></div>
</ReduxProvider>
);
}
export default App;
Estructura de archivos en Redux
Ahora que ya se tiene la tienda en el nivel superior de la aplicación, es hora la lógica de nuestra aplicación, en este caso vamos a hacer una agenda de contactos, para poder realizar un CRUD. Antes de continuar necesitamos tener en cuenta algo muy importante, el cual es la estructura de los archivos. Si bien React es una librería muy flexible frente a la estructura de archivos, Redux poner a nuestra disposición una estructura base, para a partir de eso organizar nuestros archivos.
Pensando en Ducks
Ducks es una propuesta que básicamente propone que empaquetemos un conjunto de acciones, reductores, nombres de acciones a una funcionalidad en concreto, llamando a esta agrupación duck
el cual, tranquilamente, puede empaquetarse y distribuirse como una librería.
Ahora, teniendo un poco en cuenta los patos 😅, vamos a dividir la aplicación en algo parecido, pero le llamaremos features
. De esta manera.
Feature Structure:
Como se mencionaba en un inicio, el ecosistema de React es muy flexible a la hora de organizar los archivos. Teniendo en cuenta los elementos de redux, al momento de partir una feature
debemos dividir actions
, reducer
, selectors
esto mejora la organización.
En mi caso inspirado un poco en el siguiente artículo, mi estructura es la siguiente.
View: Carpeta donde van las vistas que el usuario va a ver en pantalla, generalmente todos los componentes que son utilizando junto con el router de la aplicación. Por ejemplo, si estamos creando una aplicación de inventarios, el listado de estos productos podrían ir en una pantalla producs/list
.
Componentes: Normalmente, se querrá tener una carpeta components
en general, donde estén ubicados todos aquellos componentes, que pueden ser usados en cualquier lugar, una feature puede tener componentes que sean propios de la característica, por ejemplo el listado, de productos.
actions: En esta carpeta irán todas las acciones ligadas a esta característica.
reducer: Cada feature tiene como regla que debe exportar un solo reductor, eso no quiere decir que solo tengamos que concentrar toda lógica en un solo reducer, podemos usar combineReducers
para combinar múltiples reducer en unos solo si es que fuese necesario.
Puedes pensar en una feature
como una mini aplicación dentro de una aplicación, este se encarga de un proceso en concreto, que al final aportará un valor agregado a la aplicación en general.
Accciones
Las acciones son objetos planos que expresan una intención de cambiar el estado, eso es lo que se mencionó en el artículo anterior. Puedes pensar en una acción como un evento ocurrido en la aplicación, por ejemplo; se agregó un producto, se eliminó un contacto, cargando contactos, todos ellos describen algo que está pasando en al app.
Dicho esto podemos empezar a escribir acciones, las acciones tiene un estándar que indican que deben ser así.
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
Creadores de acciones:
Normalmente, las acciones puede despacharse de la siguiente manera.
store.dispatch({ type: "ITEM_ADDED_TO_CART", payload: 47 });
Pero en cierto punto, poner el tipo, cada vez que queramos despachar esta acción, no es muy escalable porque si se quisiera cambiar el tipo de acción, tendría que hacerlo en diferentes archivos y además, se vuelve complicado repetir lo mismo.
Ahí es donde entran loas creadores de acciones, que no son nada más que funciones encargadas de crear este objeto, un creador de acción sería el siguiente.
function doAddToDoItem(text) {
return { type: "TODO_ADDED", payload: text };
}
Entonces, cada vez que se requiera formar esta acción, solo es necesario ejecutar doAddToDoItem
.
Redux toolkit Simplifica este procedimiento con un utility llamado createAction
el cual es una HOF(higher order function) los cuales son funciones que retornan funciones.
// features/schedule/actions/schedule.actions.ts
import { createAction } from "@reduxjs/toolkit";
export const contactAdded = createAction("CONTACT_ADDED");
Ahora contactAdded
es una función que al ser disparada creará una acción de tipo CONTACT_ADDED
es importante saber que por recomendación de redux las acciones deben ser "Descripciones de eventos que ocurrieron" en lugar expresarlas en presente, como por ejemplo ADD_CONTACT
.
Payload:
Hasta este paso se creó la acción contactAdded
, pero esto no es suficiente para agregar un contacto, se necesitaría la información de ese contacto. En el caso de typescript redux toolkit
tiene un genérico para poder describir el payload.
import { createAction } from "@reduxjs/toolkit";
export const contactAdded =
createAction<{ name: string; phone: string }>("CONTACT_ADDED");
Listo ahora el primer parámetro(payload) de contactAdded
estará tipado.
Reducer
Como se mencionó anteriormente, los reducers son funciones puras que toman el estado actual y la acción para retornar un nuevo estado.
Redux toolkit exporta una función llamada createReducer
la cual facilita la creación de un reducer agregando ciertas características que facilitan el desarrollo.
import { createReducer } from "@reduxjs/toolkit";
const initalState = {
contacts: [],
};
export type ScheduleState = typeof initalState;
const reducer = createReducer(initalState, (builder) => {});
Esta sería la forma de crear un reducer con Redux toolkit
Case:
Anteriormente vimos, que cuando creamos un reducer dentro de el planteamos un switch...case
para manejar cada acción.
const reducer = (state, action) => {
switch (action) {
case "EAT": {
return {
...state,
eatCount: state.eatCount + 1,
};
}
}
};
Redux toolkit propone una forma más amigable de hacerlo, mediante un objeto builder, el cual expone una serie de métodos como addCase
con el cual le recibe como parámetros.
ActionCreator: La función generada por createAction
o una acción como tal.
Reducer: El reducer encargado solo de manejar esta acción.
Incorporando la lógica de agregar contacto, tendríamos lo siguiente.
import { createReducer } from "@reduxjs/toolkit";
import * as scheduleActions from "../actions/schedule.actions";
export interface IContact {
id: number;
name: string;
phone: string;
}
const initalState = {
contacts: [] as IContact[],
};
export type ScheduleState = typeof initalState;
const reducer = createReducer(initalState, (builder) => {
builder.addCase(scheduleActions.contactAdded, (state, action) => {
state.contacts.push({
id: state.contacts.length,
name: action.payload.name,
phone: action.payload.phone,
});
});
});
Si eres curioso. La manera en como builder encadena todos los casos sigue fluent Style😶 .
Hay algo notable aquí, y es que al parecer no estamos siguiendo el primer principio de Redux, que dice que el estado es de solo lectura, ose es inmutable. Bueno podemos ahorrarnos esa preocupación con Immer, el cual explicaré en el siguiente parte :).
Happy Coding😊
Top comments (0)