hand Inicio
hand JSBloqs
hand GutenBloqs

Utilizando useReducer

useReducer nos permite integrar una función en useState y así poder trabajar más cómodamente con estados complejos

useReducer

Una manera de definir useReducer es diciendo que es la versión completa de useState

Con useState podemos controlar el estado (es decir, guardar variables que persisten entre los distintos re-renders que ocurran en nuestra aplicación)

js
const [estado, cambiarEstado] = useState('')

La función useState acepta cualquier valor (excepto una función, pero vamos a olvidarnos de esto) que pasa a ser el valor inicial del estado

Y nos devuelve dos variables, el estado en sí y la función para canviar el estado

Ejemplos:

js
// inicializamos el hook
const [estado, setEstado] = useState({ status: 'pending', value: null })

// cambiamos el valor del estado
setEstado({ status: 'ok', value: 'whatever' })

Inciso 1

Cada vez que cambiamos el estado nuestra aplicación sufrirá un re-renderizado

Si queremos almacenar un estado sin forzar ese repintado, en lugar de useState utilizaremos el hook useRef

Inciso 2

Un error común es pensar que se ha cambiado el estado sin que sea verdad

js
const [estado, setEstado] = useState({ status: 'pending', value: null })

estado.status = 'ok'
setEstado(estado) // no hay repaint!

Aquí hemos cambiado el contenido del objeto pero NO el objeto, por lo que para React no hay razón para repintar nada

Para cambiarlo tendríamos que cambiar el objeto

js
const [estado, setEstado] = useState({ status: 'pending', value: null })
setEstado({ ...estado, status: 'ok' })

Por qué useReducer?

El nombre de useReduce ya nos da pistas de que esto tiene que ver con el método reduce()

[ tienes una entrada del Array.reduce aquí ]

Vamos con código

Pensando en la clásica aplicación ToDo, vamos a crear una lista de tareas que entraremos con un <input>

jsx
import React, { useRef, useState } from 'react'

export const App = () => {
// referencia para gestionar el input
const inputRef = useRef()

// el estado, con un valor inicial de []
const [elements, setElements] = useState([])

// funciones para añadir y quitar elementos del estado
const addElement = () => setElements([inputRef.current.value, ...elements])
const removeElement = i => setElements([...elements.slice(0, i), ...elements.slice(i + 1)])

// el jsx que gestiona la interfaz
return (
<div>
<input ref={inputRef} />
<button onClick={addElement}>✏️</button>
{elements.map((el, i) => (
<div key={'element' + i}>
<span>{el}</span>
<button onClick={() => removeElement(i)}>✔️</button>
</div>
))}
</div>
)
}

// el export default no es necesario si importamos `App` explícitamente (lo cual es recomendable)
// aquí lo añado aquí para que el codesandbox funcione out-of-the-box
export default App

Aquí tenemos un input para añadir entradas en la lista, y un botón para quitar entradas de la lista, y esa lista la guardamos como estado

Y ese estado es un array

Esto así es la mar de legible, pero voy a refactorizarlo para abstraer la lógica y tener un elemento en común

jsx
import React, { useRef, useState } from 'react'

export const App = () => {
const inputRef = useRef()
const [elements, setElements] = useState([])

// mi super nueva función
const changeElements = action =>
action.type === 'add'
? [action.value, ...elements]
: action.type === 'remove'
? [...elements.slice(0, action.i), ...elements.slice(action.i + 1)]
: []

const addElement = () => setElements(changeElements({ type: 'add', value: inputRef.current.value }))
const removeElement = i => setElements(changeElements({ type: 'remove', i: i }))

return (
<div>
<input ref={inputRef} />
<button onClick={addElement}>✏️</button>
{elements.map((el, i) => (
<div key={'element' + i}>
<span>{el}</span>
<button onClick={() => removeElement(i)}>✔️</button>
</div>
))}
</div>
)
}

export default App

Ahora tengo una función que me gestiona la lógica changeElements y dos funciones para añadir y quitar elementos más legibles

En general el código de ahora es más ofuscado, pero se entiende el concepto

Bien

Pues ahora vamos a reescribir lo mismo con useReducer

jsx
import React, { useRef, useReducer } from 'react'

export const App = () => {
const inputRef = useRef()
const [elements, setElements] = useReducer(
(state, action) =>
action.type === 'add'
? [action.value, ...state]
: action.type === 'remove'
? [...state.slice(0, action.i), ...state.slice(action.i + 1)]
: [],
[]
)

const addElement = () => setElements({ type: 'add', value: inputRef.current.value })
const removeElement = i => setElements({ type: 'remove', i: i })

return (
<div>
<input ref={inputRef} />
<button onClick={addElement}>✏️</button>
{elements.map((el, i) => (
<div key={'element' + i}>
<span>{el}</span>
<button onClick={() => removeElement(i)}>✔️</button>
</div>
))}
</div>
)
}

export default App

Es decir, lo que nos permite useReducer es integrar la función que antes teníamos fuera, dentro de la propia lógica del estado

El funcionamiento es el mismo que con reduce, recibimos primero el acumulador, que en este caso será nuestro estado, y después el nuevo valor del estado

Añadiendo complejidad al estado

Aquí se podría argumentar que el código inicial con useState es mejor, básicamente porque se entiende mejor y es más corto

Pero la extracción posterior ya sugiere que escala mejor en escenarios más complejos

Vamos a verlo añadiendo un estado para cada tarea

jsx
import React, { useRef, useReducer } from 'react'

export const App = () => {
const inputRef = useRef()

const [elements, setElements] = useReducer((state, action) => {
return action.type === 'add'
? [{ value: action.value, status: action.status }, ...state]
: action.type === 'remove'
? [...state.slice(0, action.i), ...state.slice(action.i + 1)]
: action.type === 'set'
? state.map((el, j) => (j === action.i ? { ...el, status: action.status } : el))
: []
}, [])

const addElement = () =>
setElements({
type: 'add',
value: inputRef.current.value,
status: 'pending',
})
const removeElement = i => setElements({ type: 'remove', i: i })
const doneElement = i => setElements({ type: 'set', i: i, status: 'done' })

return (
<div>
<input ref={inputRef} />
<button onClick={addElement}>✏️</button>
{elements.map((el, i) => (
<div key={'element' + i}>
<span>
{el.value} {el.status}
</span>
<button onClick={() => doneElement(i)}>✔️</button>
<button onClick={() => removeElement(i)}></button>
</div>
))}
</div>
)
}

export default App

Al igual que antes, este código nos permite entenderlo todo muy rápidamente excepto la función useReducer

Esto es bueno ya que tendremos una idea de qué hace esa parte del código y podremos seguir adelante

Si lo hacemos con useState estaremos exactamente en la misma situación

jsx
import React, { useRef, useState } from 'react'

export const App = () => {
const inputRef = useRef()

const [elements, setState] = useState([])

const setElements = action =>
setState(
action.type === 'add'
? [{ value: action.value, status: action.status }, ...elements]
: action.type === 'remove'
? [...elements.slice(0, action.i), ...elements.slice(action.i + 1)]
: action.type === 'set'
? elements.map((el, j) => (j === action.i ? { ...el, status: action.status } : el))
: []
)

const addElement = () =>
setElements({
type: 'add',
value: inputRef.current.value,
status: 'pending',
})
const removeElement = i => setElements({ type: 'remove', i: i })
const doneElement = i => setElements({ type: 'set', i: i, status: 'done' })

return (
<div>
<input ref={inputRef} />
<button onClick={addElement}>✏️</button>
{elements.map((el, i) => (
<div key={'element' + i}>
<span>
{el.value} {el.status}
</span>
<button onClick={() => doneElement(i)}>✔️</button>
<button onClick={() => removeElement(i)}></button>
</div>
))}
</div>
)
}

export default App

Es decir, aquí lo entendemos todo a la perfección excepto la función setElements, que es la parte que antes estaba integrada en el useReducer

Por lo tanto la decisión no es si utilizar useState o useReducer, sino en cómo organizar la modificación de nuestro estado

  • Podemos trabajar con distintos estados

  • Podemos trabajar con un único estado y modificarlo con distintas funciones

  • Podemos trabajar con un único estado y modificarlo con una única función

Depende de nuestras necesidades escogeremos una estrategia u otra, y entonces es cuando el useReducer nos puede dar la oportunidad de ahorrarnos algunas líneas de código que siempre se agradece

El objetivo siempre tiene que ser augmentar la legibilidad del código, o en otras palabras, aplicar todo lo que se puede el principio KISS

🙋‍♂️

draw of me

Hola, tienes en mente desarrollar una web?

Si quieres, te ayudo

11ty para webs ultra-rápidaseleventy js
Gatsby para webs complejasgatsby js
WordPress para webs para el usuario finalwordpress

Escríbeme

Lista de correo: escribo algo de tanto en cuanto