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)
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:
// 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
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
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>
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
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
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
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
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
🙋♂️
Lista de correo: escribo algo de tanto en cuanto