¿Recuerdas que siempre decimos que la programación es como Taco Bell? ¡Siempre son los mismos ingredientes utilizados de una manera diferente! En este caso particular, vamos a confiar mucho en los Eventos para crear toda la arquitectura de la aplicación.
Sabemos que todavía estás aprendiendo React. Los states (estados) y las props (propiedades) pueden ser confusos, y ahora, con Flux, las cosas se van a poner un poco más difíciles ¡Pero es por una buena causa!
Sin Flux, no puedes crear aplicaciones React medianas o grandes porque todo se desorganizará bastante rápido.
Además, dos vistas diferentes no pueden enviar datos entre sí como lo hacen los componentes (utilizando props) porque todas las vistas son hermanas y React Router las está instanciando. Necesitamos tener un store común compartido entre todas las vistas que vamos a llamar "The Store."
Aquí hay una lista de todas las ventajas de usarlo:
Vistas/Views (Components) | Cada componente React que llama a cualquier acción Flux es llamada una vista. La razón para llamar a esos componentes de una manera diferente es porque se supone que los componentes de React se comunican entre sí a través de sus props (sin Flux). Una vez que un componente React esté hard coded a Flux, no podrás reutilizar ese componente en el futuro (en este o en cualquier otro desarrollo). |
Acciones (Actions) | Las acciones pueden ser activadas por componentes (cuando el usuario hace clic o interactúa con la aplicación) o por el sistema (por ejemplo, la funcionalidad de guardado automático). Las acciones son el primer paso de cualquier flujo de trabajo de Flux y siempre deben enviarse al Store. |
Store | El store contiene todos los datos de la aplicación. Maneja todo lo que recibe el despachador y determina la forma en que se deben almacenar y recuperar los datos. |
El siguiente proyecto es una aplicación de To-Do List (lista de tareas) con 3 historias de usuario principales:
Para codificar esta lista de tareas tenemos que crear 4 archivos:
Al final, trabajar con Flux tiene que convertirse en algo tan automático como andar en bicicleta.
Para poder tomar el control del flujo de los datos en nuestra aplicación utilizaremos un reducer
para agrupar las funciones y la lógica de la aplicación (actions) junto con los datos que manejan y que tienen que estar disponible para los componentes (state).
Por ahora solo diremos que el reducer es una función que genera un estado nuevo cada vez que se ejecuta y lo que haga dependerá de la información que reciba en la función action
. Esto nos permitirá llamar a las actions
para actualizar el estado como lo indica el patrón flux. Para entender en detalle como funciona un reducer, puedes leer esté artículo donde lo explicamos a profundidad.
1// Esta es la función reducer 2const TaskReducer = (state, action) => { 3 // Dependiendo del type de la acción realiza una tarea distinta 4 switch (action.type) { 5 case "add": 6 return [...state, action.payload]; 7 case "remove": 8 let newState=[...state] 9 newState.splice(action.index, 1); 10 return newState 11 default: 12 return state; 13 } 14};
El siguiente paso es hacer que esta función esté disponible para todos los componentes de mi aplicación, para eso utilizaremos un contexto con el hook useReducer
, el cual nos va a permitir crear el estado y la función actions
para ponerla a disposición del resto de la aplicación.
1//TaskContext.jsx 2import { useReducer, createContext } from "react"; 3 4// Creamos el contexto vacío 5const TaskContext = createContext(null); 6 7const TaskReducer = (state, action) => { 8 // Aquí va el reducer que se definió anteriormente👆 9}; 10 11// Crearemos un componente que va a envolver nuestra aplicación en el contexto 12export function TaskProvider({ children }) { 13 // Creamos el state 'tasks' y el despachador 'taskActions' 14 // adicionalmente pasamos como estado inicial un arreglo vacío 15 const [tasks, taskActions ]= useReducer(TaskReducer, []); 16 return ( 17 {/* Creamos el contexto con nuestro state y actions */} 18 <TaskContext.Provider value={{tasks, taskActions}}>{children}</TaskContext.Provider> 19 ); 20} 21 22// Es necesario exportar el contexto para usarlo en otros componentes 23export default TaskContext;
Ya con esto tenemos listo nuestro contexto con las tasks, ahora solo falta envolver nuestra aplicación en este componente para empezar a utilizarlo.
1//index.jsx 2import React from "react"; 3import ReactDOM from "react-dom/client"; 4import App from "./App"; 5import { TaskProvider } from "./TaskContext.jsx"; 6 7ReactDOM.createRoot(document.getElementById("root")).render( 8 <React.StrictMode> 9 <TaskProvider> 10 <App /> 11 </TaskProvider> 12 </React.StrictMode>, 13); 14
Ahora solo queda llamar al contexto desde los componentes que necesiten hacer uso de nuestras tareas.
Para ello usaremos un componente que muestre una caja de texto y un botón que realiza la acción de agregar la tarea, todo dentro de un formulario para facilitar el manejo del evento de envío(submit).
Todo esto es básico de un formulario en react, pero como queremos utilizar las actions
del contexto, necesitamos llamarlo en el componente.
1import { tasks, useContext } from "react"; 2import TaskContext from "./TaskContext.jsx"; 3 4export default function AddItem() { 5 const { taskActions } = useContext(TaskContext); 6 // A partir de este punto tenemos disponibles las actions del reducer 7 // ... 8}
Para hacer uso de estas actions
se llama a la función y se le pasa como parámetro un objeto con la propiedades de la acción que queremos realizar, siendo la mas importante type
que indica la acción especifica a ejecutar. El resto de las propiedades son datos opcionales que pueden ser requeridos por la acción.
1// AddItem.jsx 2import { useContext } from "react"; 3import TaskContext from "./TaskContext.jsx"; 4 5export default function AddItem() { 6 // Hacemos uso del contexto y accedemos a la función 'taskActions' 7 const { taskActions } = useContext(TaskContext); 8 function handleAddTask(e) { 9 e.preventDefault(); 10 // Llamamos al actions especificándole 'type' 11 // asi como también la tarea que se va a agregar 12 let textbox = e.target.elements.task; 13 taskActions({ type: "add", payload: textbox.value }); 14 textbox.value = ""; 15 } 16 return ( 17 <li> 18 <form onSubmit={handleAddTask}> 19 <input name="task" type="text"/> 20 <button type="submit">+</button> 21 </form> 22 </li> 23 ); 24}
De la misma forma como accedemos a taskActions
, también podemos acceder al objeto tasks
que contiene el estado con la lista. Igual que antes, debemos hacer uso de useContext
en nuestro componente.
1import { useContext } from "react"; 2import "./App.css"; 3import TaskContext from "./TaskContext.jsx"; 4import ListItem from "./ListItem.jsx"; 5import AddItem from "./AddItem.jsx"; 6 7export default function App() { 8 // Accedemos al contexto, pero esta vez solo vamos a usar 'tasks' 9 const {tasks} = useContext(TaskContext); 10 11 return ( 12 <main> 13 <h2>Todo list</h2> 14 <ul className="list-group w-50"> 15 <AddItem /> 16 {tasks.map((task, index) => ( 17 <ListItem key={index} task={task} index={index} /> 18 ))} 19 </ul> 20 </main> 21 ); 22}
Puedes notar que aparece el componente AddItem
que vimos previamente y desde donde se pueden agregar tarea. Luego de eso se hace el renderizado de la lista con la función map
, pero notamos que se esta usando un componente ListItem
para mostrar los elementos, no solo eso sino que ahi también corresponde hacer la eliminación de la tarea, veamos ese componente.
Si bien el renderizado es básico (un elemento li
con el texto y un botón), lo interesante es como hacemos la eliminación del item con las actions.
Todo comienza cuando el usuario haga clic en el icono de la papelera. Es por eso que necesitamos iniciar nuestra aplicación escuchando el típico evento onClick en el botón de eliminar.
1 onClick={() => taskActions({ type: "remove", index })}
Notamos que el llamado al action es parecido al que usamos para agregar items, pero se le esta pasando un parámetro distinto llamado index
, que le indica al dispatcher que elemento va a eliminar. Asi como vimos en ambos ejemplos, podemos pasar la data que necesite nuestra action al momento de llamarla como parámetros adicionales.
1import { useContext } from "react"; 2import TaskContext from "./TaskContext.jsx"; 3 4export default function ListItem({ task, index }) { 5 const { taskActions } = useContext(TaskContext); 6 7 return ( 8 <li> 9 {task} 10 <button 11 onClick={() => taskActions({ type: "remove", index })} 12 > 13 {/* Icono de papelera */} 14 <i className="bi bi-trash3"></i> 15 </button> 16 </li> 17 ); 18}
Ya hemos implementado la lógica de nuestra aplicación en un contexto aplicando el patrón flux, permitiendo su uso en distintos componentes. A continuación podemos ver el resultado final.