Si has trabajado con React en una aplicación compleja, te habrás enfrentado a lo complicado que es manejar los datos, o el estado global de aplicación. Redux viene a representar una solución a este problema. Esta librería va muy de la mano con React, aunque no se limita, ya que es agnóstica. Esto quiere decir que podrías usarla con Angular, VUE.js, etc.
Aunque es ampliamente usado, Redux, tal como fue concebido, trajo algunos problemas de experiencia de desarrollo, tales como:
Acá es donde entra Redux Toolkit (o RTK), para mejorar nuestra experiencia de desarrollo, además de hacer nuestras aplicaciones más pequeñas y cumpliendo su cometido.
Para ilustrar lo sencillo que es empezar a trabajar con RTK, vamos a crear nuestro primer "slice”. RTK sigue el patrón ducks y combina reducers, actions y constantes en un solo archivo (slice).
Vamos a conectar una aplicación de lista de tareas a Redux y usaremos un slice
para el manejo de nuestros tasks
:
Lo primero que haremos será clonar el siguiente repositorio: https://github.com/musartedev/redux-toollkit-todo y empezaremos en la rama start
:
git clone https://github.com/musartedev/redux-toollkit-todo.git
cd redux-toollkit-todo
git checkout start
Ahora instalaremos las dependencias necesarias e iniciaremos el proyecto:
npm i
npm start
Puedes explorar la lista de componentes que están disponibles:
import { TodoItem } from'./TodoItem';
import'../styles/TodoList.css';
exportconst TodoList = () => {
// Arreglo temporal para maquetar const mockArray = Array(3).fill({ id: 1, text: 'Item', done: false });
return (
<sectionclassName='TodoList'><ul>
{mockArray.map(({ id, text, done }) => (
<TodoItemkey={id}text={text}done={done} />
))}
ul>section>
);
};
import'../styles/TodoItem.css';
exportconst TodoItem = ({ text, done }) => {
const doneClass = done ? 'TodoItem--done' : '';
return<liclassName={`TodoItem ${doneClass}`}>{text}li>;
};
import { useState } from'react';
import'../styles/TodoInput.css';
exportconst TodoInput = () => {
const [text, setText] = useState('');
const handleInputChange = (e) => {
setText(e.target.value);
};
return (
<sectionclassName='TodoInput'><labelclassName='TodoInput-label'>Añade una tarealabel><inputclassName='TodoInput-input'value={text}onChange={handleInputChange}placeholder='Escribe y presiona enter'
/>section>
);
};
Cada una de nuestras tareas tendrá la siguiente estructura:
const task = {
id, // String identificador de la tarea.
text, // String con el texto que se muestra al usuario.
done, // Booleano que indica si la tarea fue completada.
}
Si vas al navegador, vas a notar que por ahora nuestra lista es no funcional, para arreglarlo vamos a utilizar RTK.
Utilizaremos npm
para instalar nuestra librería RTK. Adicionalmente, instalaremos uuid, librería que nos permitirá crear identificadores únicos para nuestras tareas:
npm install react-redux @reduxjs/toolkit uuid
En nuestra carpeta src
, vamos a crear una carpeta llamada slices
, y nuestro primer archivo allí llamado taskSlice.js
.
RTK nos proporciona una API llamada createSlice
, que es un método de tipo helper que nos permite crear un “pedazo” del store. En este caso, será el pedazo que contendrá las tareas de nuestra lista.
El método recibe como parámetro un objeto con las siguientes propiedades principales:
Esto lo vemos mejor en la práctica:
import { createSlice } from'@reduxjs/toolkit';
exportconst tasksSlice = createSlice({
name: 'tasks',
initialState: [],
reducers: {
// Reducer y acciones asociadas.
},
});
Ahora, pensemos un momento a ver qué acciones podemos hacer con nuestra lista de tareas:
Agreguemos el primer caso a nuestro objeto reducers
:
import { createSlice } from'@reduxjs/toolkit';
import { v4 as uuidv4 } from'uuid';
exportconst tasksSlice = createSlice({
name: 'tasks',
initialState: [],
reducers: {
addTask: (state, action) => {
// Nuestros reducers reciben el estado actual y la información de action.// (contiene el payload.// Creamos nuestra nueva tarea:const newTask = {
id: uuidv4(), // Nos apoyamos en la librería uuid para asignar un identificador.
text: action.payload.text, // Recibimos el texto en el payload de la acción.
done: false, // Iniciamos la tarea como no completada.
};
state.push(newTask); // La incluimos en el arreglo de tareas de nuestro estado.
},
},
});
// Exportamos los actions desde el slice que acabamos de crear:exportconst { addTask } = tasksSlice.actions;
// Por defecto exportamos la propiedad reducer desde el slice que acabamos de crear:exportdefault tasksSlice.reducer;
import React from'react';
import ReactDOM from'react-dom/client';
import { Provider } from'react-redux'; // Importamos el provider.import { configureStore } from'@reduxjs/toolkit'; // Importamos el método para configurar el store desde RTK.import App from'./App';
import taskReducer from'./slices/taskSlice'; // Importamos nuestro Slice (desde el default export).const store = configureStore({
reducer: {
tasks: taskReducer, // Por ahora solo tenemos este slice, pero acá podremos agregar otros.
},
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
{/* Agregamos el provider a nuestr app */}
);
Ahora solo nos falta consumir el estado y disparar nuestra acción para agregar una nueva tarea:
import { useState } from'react';
import { useDispatch } from'react-redux'; // Importamos el hook useDispatch.import { addTask } from'../slices/taskSlice'; // Importamos nuestra accion para agregar una tarea.import'../styles/TodoInput.css';
exportconst TodoInput = () => {
const dispatch = useDispatch(); // Inicializamos el disparador.const [text, setText] = useState('');
const handleInputChange = (e) => {
setText(e.target.value);
};
// Se ejecuta cuando el usuario presiona una tecla:const handleKeyDown = (e) => {
// Evaluamos que la tecla presionada sea "Enter":if (e.key === 'Enter') {
// Disparamos el action addTask y enviamos el texto como payload.
dispatch(addTask({ text }));
// Borramos el texto del input.
setText('');
}
};
return (
<sectionclassName='TodoInput'><labelclassName='TodoInput-label'>Añade una tarealabel><inputclassName='TodoInput-input'value={text}onChange={handleInputChange}placeholder='Escribe y presiona enter'onKeyDown={handleKeyDown} // Seejecutacuandoelusuariopresionaunatecla.
/>section>
);
};
Vayamos ahora la lista de tareas y mostremos lo que hay en nuestro estado:
import { useSelector } from'react-redux'; // Importamos el hook para acceder al estado.import { TodoItem } from'./TodoItem';
import'../styles/TodoList.css';
exportconst TodoList = () => {
// Obtenemos la lista de tareas del estadoconst taskList = useSelector((state) => state.tasks);
return (
<sectionclassName='TodoList'>
{/* Mostramos los elementos o un texto por defecto si la lista está vacía */}
{taskList.length > 0 ? (
<ul>
{taskList.map(({ id, text, done }) => (
<TodoItemkey={id}id={id}text={text}done={done} />
))}
ul>
) : (
<p>Empieza a añadir tareas ✨ p>
)}
section>
);
};
Pasemos al navegador y veremos que tenemos la funcionalidad para agregar tareas lista:
Ya que hemos añadido nuestras tareas, debemos tener una manera de completarlas. Para ello, vamos de nuevo a nuestro slice
y agregamos otro caso en nuestro objeto reducers
:
import { createSlice } from'@reduxjs/toolkit';
import { v4 as uuidv4 } from'uuid';
exportconst tasksSlice = createSlice({
name: 'tasks',
initialState: [],
reducers: {
addTask: (state, action) => {
const newTask = {
id: uuidv4(),
text: action.payload.text,
done: false,
};
state.push(newTask);
},
toggleTask: (state, action) => {
// Buscamos el index de la tarea a marcar como completada / no completada.const taskIndex = state.findIndex(
(task) => task.id === action.payload.id
);
// Si la tarea existe, cambiamos el estado de su propiedad done.if (taskIndex >= 0) {
state[taskIndex].done = !state[taskIndex].done;
}
},
},
});
exportconst { addTask, toggleTask } = tasksSlice.actions; // Agregamos el nuevo caso al export.exportdefault tasksSlice.reducer;
Disparemos nuestra nueva acción al hacer clic en alguna tarea:
import { useDispatch } from 'react-redux'; // Importamos el hook useDispatch.
import { toggleTask } from '../slices/taskSlice'; // Importamos nuestro action.
import '../styles/TodoItem.css';
export const TodoItem = ({ id, text, done }) => {
const dispatch = useDispatch(); // Inicializamos el disparador.
// Se ejecuta cuando el usuario hace clic en la tarea.
const handleOnClick = () => {
// Dispara el action toogleTask con el id de la tarea como payload.
dispatch(toggleTask({ id }));
};
const doneClass = done ? 'TodoItem--done' : '';
return (
{/* Agregamos el handle del evento onClick */}
{text}
);
};
Vamos nuevamente al navegador. Ya debemos tener lista nuestra funcionalidad:
¿Ves lo sencillo que es trabajar con slices y RTK? En un solo archivo manejamos cada pedacito de aplicación. Si bien esta es sencilla, mientras tenemos apps más complejas, llevar esa separación de responsabilidades nos ayuda a ubicarnos mejor en el código.
Como habrás podido notar, nos falta nuestro último caso. Con lo aprendido en este post, ¿cómo agregarías la funcionalidad de eliminar una tarea de la lista? Espero tu solución en los comentarios 😃
Domina esto y mucho más en el Curso Profesional de React.js y Redux
Muy buen tutorial para empezar, muchas gracias
export const tasksSlice = createSlice({ name: 'tasks', initialState: [], reducers: { addTask: (state, action) => { const newTask = { id: uuidv4(), text: action.payload.text, done: false, }; state.push(newTask); }, toggleTask: (state, action) => { // Buscamos el index de la tarea a marcar como completada / no completada. const taskIndex = state.findIndex( (task) => task.id === action.payload.id ); // Si la tarea existe, cambiamos el estado de su propiedad done. if (taskIndex >= 0) { state[taskIndex].done = !state[taskIndex].done; } }, deletetask=(state, action)=>{ const taskIndex = state.findIndex( (task) => task.id === action.payload.id ); return state.splice(taskIndex,1) } }, }); export const { addTask, toggleTask, deletetask } = tasksSlice.actions; // Agregamos el nuevo caso al export. export default tasksSlice.reducer;`