El manejo de estado dentro de React es uno de los conceptos que nos ayudará a manejar la data dentro de nuestros componentes y vistas. Muy probablemente lo primero que se te venga a la mente para tener control sobre este sea el hook useState
.
No hay ningún problema con useState
si hablamos del estado local de tus componentes, pero el usarlo para el estado global de tu aplicación te dará muchos dolores de cabeza. Es por eso que te vengo a presentar a su versión con superpoderes: useReducer.
Este grandioso hook nos provee un dispatcher para poder actualizar el estado de formas que nosotros establecemos previamente. Y, a su vez, también nos permite acceder al estado que contiene.
🧾 Aunque al escuchar el nombre de “reducer” se nos venga a la mente el nombre de Redux, esta no es una alternativa a la herramienta, simplemente React se inspiró en Redux para crear este hook
El problema de useState es el que nosotros creamos lógica extra en nuestro componente por cada acción que deba actualizar el estado, mientras que useReducer se lleva toda esa lógica a un archivo aparte y solo proporciona esos métodos a cada componente.
Visualmente se parece mucho a useState pero requiere una configuración adicional (crear un reducer) para que puedas sacarle todo el provecho
Su sintaxis es la siguiente:
const [state, dispatch] = useReducer(reducer, initialState)
Por ejemplo, vamos a hacer el típico componente de un contador que suma o resta una unidad.
Con useState haríamos algo así 👇🏻
// counter.jsximport React, { useState } from'react'const Counter = () => {
const [count, setCount] = useState(0)
const increment = () => {
setCount(count + 1)
}
const decrement = () => {
setCount(count - 1)
}
return(
<div><buttontype="button"onClick={increment}> + </button><buttontype="button"onClick={decrement}> - </button><p>The current count is: {count}</p></div>
)
}
useReducer por su parte, nos permite tener algo más limpio como esto
// Counter.jsximport React, { useReducer } from'react'const initialState = {count: 0}
const reducer = (state, action) => {
switch (action.type) {
case'INCREMENT':
return {count: state.count + 1};
case'DECREMENT':
return {count: state.count - 1};
default:
thrownewError("The option doesn't exist");
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return(
<div><buttontype="button"onClick={() => dispatch({ type: "INCREMENT" })}>
+
</button><buttontype="button"onClick={() => dispatch({ type: "DECREMENT" })}>
-
</button><p>The current count is: {state.count}</p></div>
)
}
💻 Demo funcionando en este codepen
De esa forma los componentes solo se encargan de pintarse en pantalla y la lógica llega preparada desde otro lugar de nuestra App.
Sin embargo, seguimos teniendo el detalle que no estamos manejando un estado global, simplemente separamos las funciones en lugares diferentes.
¿Cómo es que esto escala mejor que useState? Hasta pareciera que useReducer hace un código mucho más difícil de mantener. 😰
Como habrás visto en el ejemplo anterior, useReducer no es tan eficaz para manejar estados locales, lo recomendable es combinarlo con React Context para crear un estado global.
Vamos a crear una App que pueda manipular la información de los productos de un e-commerce mientras te explico como realizar la configuración de context con useReducer.
Este archivo contendrá el estado global junto con el reducer que nos ayudará a actualizarlo, lo deberás poner en tu carpeta src(source) para mayor comodidad.
Su estructura puede verse un tanto compleja pero aquí te explico para que sirve cada cosa.
// importamos React junto a useReducer y createContextimport React, { useReducer, createContext } from'react'// Recuerda que para acceder a nuestro contexto desde multiples lugares de // nuestra app, deberemos exportarloexportconst ProductsContext = createContext()
// Aquí vamos a crear el estado inicial que tendrá el context y consumiremos con// useReducerconst initialState = {
products: [
{
id: Date.now().toString(),,
img: "https://picsum.photos/100",
name: "Shoes",
description: "Shoes for people",
price: 25,
quantity: 2
}
]
}
// Este será nuestro reducer que nos ayudará a crear o eliminar los productos// dependiendo el action.type que recibaconst reducer = (state, action) => {
switch(action.type){
case"ADD_PRODUCT":
return{
products: [...state.products, action.payload]
}
case: "REMOVE_PRODUCT":
return{
products: state.products.filter((product) => product.id !== action.payload),
}
default:
return {
state
}
}
}
// De igual forma, exportaremos el provider que envolvera todos los componentes// de nuestra appexportconst ProductsContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
// Dentro del value del provider colocaremos el state y el dispatch para // manejar el estado globalreturn (
<ProductsContext.Providervalue={[state,dispatch]}>
{children}
</ProductsContext.Provider>
);
};
Este paso es súper importante para que puedas consumir el contexto correctamente desde cualquier lugar de la App (recuerda que también debes importarlo en cada componente que lo necesite)
Vamos a usar 3 componentes dentro del provider los cuales detallaremos más adelante:
// index.jsimport React from'react'// Las rutas son relativas a la organización de tus componentesimport { CreateProduct } from"./components/CreateProduct"import { DeleteProduct } from"./components/DeleteProduct"import { ListItems } from"./components/ListItems"// importamos el context provider para poder consumirloimport { ProductsContextProvider } from'./GlobalState.js'// Envolvemos a los componentes que van a tener acceso a este estado// En este caso será toda nuestra aplicaciónconst App = () => {
return(
<ProductsContextProvider>
<CreateProduct />
<DeleteProduct />
<ListItems />
</ProductsContextProvider>
)
}
Como habrás visto, nuestro reducer solo contempla la creación, eliminación y lectura de la data por lo que vamos a crear un componente por cada uno de ellos
Solo faltaría crear el método update para que sea un CRUD, pero eso se te queda de tarea. 😉
CreateProduct.jsx (Create):
// CreateProduct.jsx// Vamos a importar React como de costumbre y también el hook useContext que nos// permitira acceder a los valores del context provider, pero este solo// funciona si le pasamos nuestro contexto que creamos al incioimport React, { useContext } from"react"import { ProductsContext } from"../GlobalState.js"exportconst CreateProduct = () => {
const [state, dispatch] = React.useContext(ProductsContext)
// El evento onSubmit del form nos permitirá mandar la data mediante el dispatch// este debe ser un objeto con dos propiedades:// type - El cual será uno de los método del reducer// payload - la data de nuestro producto que queremos crearreturn(
<form onSubmit={(e) => {
e.preventDefault()
dispatch({
type: "ADD_PRODUCT",
payload: {
id: Date.now().toString(),,
img: e.target.img.value,
name: e.target.name.value,
description: e.target.description.value,
price: Number(e.target.price.value),
quantity: Number(e.target.quantity.value),
}
})
}}>
<input required placeholder="img" type="text" name="img" />
<input required placeholder="name" type="text" name="name" />
<input required placeholder="description" type="text" name="description" />
<input required placeholder="price" type="number" name="price" />
<input required placeholder="quantity" type="number" name="quantity" />
<button type="submit">Create item</button>
</form>
)
}
ListItems.jsx (Read):
// ListItems.jsximport React, { useContext } from"react"import { ProductsContext } from"../GlobalState.js"// Repetimos lo mismo que en el componente anterior// Aquí solo vamos a consumir el estado, por lo que no es necesario incluir el dispatchexportconst ListItems = () => {
const [state] = useContext(ProductsContext)
return(
<ul>
{
state.products.map(product =><likey={product.id} >{product.name}</li> )
}
</ul>
)
}
DeleteProduct.jsx (Delete):
// DeleteProduct.jsximport React, { useContext } from"react"import { ProductsContext } from"../GlobalState.js"exportconst DeleteProduct = () => {
const [state, dispatch] = useContext(ProductsContext)
// En este componente solo vamos a presentar el estado mediante un select en el// cual podrás eliminar los items según el id que tenga// El dispatch en este caso solo va a recibir el id mediante el payload y // su type será "REMOVE_PRODUCT"return(
<formonSubmit={(e) => {
e.preventDefault()
dispatch({
type: "REMOVE_PRODUCT",
payload: e.target.id.value
})
}}>
<selectname="id"><optionvalue=""selected> Select an option </option>
{state.products.map(product =><optionvalue={product.id}>{product.id} {product.name}</option>)}
</select><buttontype="submit"> Delete </button></form>
)
}
Ya tienes toda la lógica necesaria para que este proyecto funcione, visualmente no se ve tan bien así que deja volar tu imaginación y dale ese toque. Aquí abajo te dejo una muestra de como me quedó este mini-proyecto estilizado
En este codepen encontrarás la demo
Con un solo hook y el context API logramos crear una app que fácilmente te puede ayudar a mantener una lista de productos en un e-commerce, las entradas de un blog o crear una ToDo App (como la que vemos con el profe JuanDC en esta clase).
Ahora imagínate el potencial que puedes sacarle a React si conoces todas sus herramientas, las cuales aprenderás sin dificultad en los siguientes cursos 👇🏻
Yo sólo edité el contador porque no quería números negativos haha
const initialState = {count: 0} const reducer = (state, action) => { console.log(state) switch (action.type) { case 'INCREMENT': return {count: state.count + 1}; case 'DECREMENT': return state.count >= 1 ? {count: state.count - 1} : {count: 0}; default: throw new Error("The option doesn't exist"); } } const App = () => { const [state, dispatch] = React.useReducer(reducer, initialState) return( <div><button type="button" onClick={() => dispatch({ type: "INCREMENT" })}> + </button><button type="button" onClick={() => dispatch({ type: "DECREMENT" })}> - </button><p>The current count is: {state.count}</p></div> ) } ReactDOM.render(<App />, document.getElementById("App"))
Excelente blog Leo! 💚
realamente me quedó grande hacer el update del producto, pero me pareció muy bueno el tutorial, si hay aportes paraterminarlo se agradecen!!!
Buena publicación