15

Tutorial de useReducer con React Context

49090Puntos

hace un año

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.

¿Qué tiene de especial 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)
  • state: se encarga de proporcionar el estado que mantiene
  • dispatch: es el método que va a leer la instrucción que buscas realizar y pasarla al reducer
  • reducer: es una función que recibe el estado inicial y una acción realizará una operación u otra en el estado. Siempre de manera inmutable, no podemos modificar el estado, si no crear una copia a partir del anterior.
  • initialState: es el estado inicial en el cual vas a definir tu punto de entrada

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. 😰

Sácale el mejor provecho con context ⚡

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.

1️⃣ Crea un archivo GlobalState.js

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>
  );
};

3️⃣ Envuelve tus componentes con el 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>
  )
}

4️⃣ Crea tus componentes que manipularán la data

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>
  )
}

5️⃣ Pule los detalles de esta lista de productos

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

Demo live

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 👇🏻

Leonardo de los angeles
Leonardo de los angeles
LeoCode0

49090Puntos

hace un año

Todas sus entradas
Escribe tu comentario
+ 2
Ordenar por:
4
29605Puntos

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! 💚

2
29696Puntos

realamente me quedó grande hacer el update del producto, pero me pareció muy bueno el tutorial, si hay aportes paraterminarlo se agradecen!!!

1

Unfortunately, I was unable to update the product myself, but I did find the instruction to be rather helpful; thus, any funds to complete the project would be much appreciated. connections game

1
4Puntos

This code is really hard. It’s scary looking tunnel rush I have to go to tunnel rush to get more motivation but I’m so frustrated.