useReducer para el manejo de formularios
Tags: react, usereducer
Published: 2021-07-16 01:31:04

Hace unos días atrás venía desarrollando modales con formularios dentro. Nada del otro mundo, algunos con mas o menos cosas.
Hasta que llegó un modal con muchos formularios dentro, el mismo incluía un formulario principal, un tab en el cual había 5 pestañas, cada una con sus respectivos formularios. No solo eso, sino que además algunos de ellos tenian campos que se podían editar/agregar/eliminar. Por otro lado, los datos ingresados debían persistir al cambiar entre pestañas, todo esto me daba la pauta de que necesitaba cambiar de estrategia y usar la herramienta correcta.

## Context no fué la opción

En un principio pensé en utilizar context junto con su custom hook, useContext. Lo llegué a implementar pero resultó en mas de un dolor de cabeza debido a una particularidad en la implementación de los Tabs (el proyecto usa Material-Ui). Resumen: Causaba re-renders innecesarios, useMemo/Memo no solucionaban el problema ya que un componente de Material-Ui estaba por asi decirlo "desmontando y volviendo a montar uno de los componentes por lo que no tenia sentido usar esas opciones. Y por si no fuera poco la ui se notaba con bajos fps… los gamers me entenderán en especial en desplegables donde mas se necesita la fluidez.

Otro punto muy importante por que context no era la solución para este caso particular fué que… como dice la documentación, el objetivo de este hook es compartir data/funcionalidad entre el arbol de componentes. Más bien en este caso useReducer era la opción correcta ya que cumple la misma función que context (almacenar la data de los form) pero de manera local ya que no iba a ser  compartida en otros componentes.

## Solución: Hola useReducer

Para este caso el hook useReducer era lo que necesitaba. Por varias razones.
* La primera es que al estar fuera del componente, no es afectado por los renders del mismo
* Simplifica el código al separar la lógica
* Es mucho mejor para manejar estados complejos

Y ahora un copy&paste salvaje de la documentación de react sobre useReducer

> useReducer a menudo es preferible a useState cuando se tiene una lógica compleja que involucra múltiples subvalores o cuando el próximo estado depende del anterior. useReducer además te permite optimizar el rendimiento para componentes que activan actualizaciones profundas, porque puedes pasar hacia abajo dispatch en lugar de callbacks.

## Código de ejemplo
Este ejemplo utiliza Material UI, solo para darle un toque visual. Consiste en 3 inputs, cada uno tiene su evento onChange en el cual dispachan un action, de esa forma actualizan el state. Y luego un botón el cual para este ejemplo simplemente hace un console.log del state.

El propósito de este ejemplo es mostrar como se implementa en un componente, como se guarda los valores en el state y como se obtienen los mismos.

Este ejemplo consta de 2 archivos, *formReducer.js* y *App.js*.

**formReducer.js**
```
export const initialState = {
  nombre: '',
  apellido: '',
  nacionalidad: ''
}

export const reducer = (state, action) => {
  switch (action.type) {
    case "ON_CHANGE":
      return {
        ...state,
        [action.payload.nameProp]: action.payload.value
      }
    default:
      throw new Error()
  }
}
```
### Explicación formReducer.js
**initialState**: Consiste en un objeto el cual puede iniciar vacio {}. Aunque recomiendo que contenga todos los datos con sus valores inicializados. Este initialState luego será pasado como parametro a nuestro hook useReducer

**reducer**: Función que recibe 2 parametros, el `state` y `action`.
- El state es el estado anterior que luego será combinado con los nuevos valores y finalmente retornado como un nuevo state, o según el caso puede que solo se retorne el state sin ser modificado.
- El action consiste en un objeto con 2 propiedades, type y un objeto comunmente llamado payload.
La propiedad type es un string que sirve para ser diferenciado del resto de actions y en base a eso, modificar el state de la forma que sea mas conveniente segun sea el caso.

Forma de un action:
`{ type: 'NOMBRE_ACTION', {name: 'nombre'} }`

En el cuerpo de la funcion reducer, siempre retornamos un nuevo state con los valores nuevos o simplemente devolvemos el state mediante switch.
Particularmente en este caso, en el default lanzamos un error, para que literalmente explote la app, esto, es preferible en modo desarrollo para asi saber si, le estamos pasando un action type que no exista, entrará en el default y lanzará el error, avisandonos de esa forma que algo no está bien. Otra alternativa es simplemente devolver el state.

**App.js**
```
import { useReducer } from 'react'
import Grid from '@material-ui/core/Grid';

import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';

import {initialState, reducer} from './formReducer'

function App() {
  const [state, dispatch] = useReducer(reducer, initialState)

  const onSubmit = () => {
    console.log(state)
  }
  return (
    <>
      <Grid container spacing={3}>
        <Grid item xs={12}>
          <TextField
            label="Nombre"
            variant="outlined"
            onChange={e => dispatch({type:"ON_CHANGE", payload: { nameProp: "nombre", value: e.target.value } }) }
          />
        </Grid>
        <Grid item xs={12}>
          <TextField
            label="Apellido"
            variant="outlined"
            onChange={e => dispatch({type:"ON_CHANGE", payload: { nameProp: "apellido", value: e.target.value } }) }
          />
        </Grid>
        <Grid item xs={12}>
          <TextField
            label="Nacionalidad"
            variant="outlined"
            onChange={e => dispatch({type:"ON_CHANGE", payload: { nameProp: "nacionalidad", value: e.target.value } }) }
          />
        </Grid>
        <Grid item xs={12}>
          <Button variant="contained" color="primary" onClick={onSubmit}>Enviar</Button>
        </Grid>
      </Grid>

    </>
  );
}

export default App;
```

### Explicacion App.js
Como para este caso estamos usando Material-UI hay mucho código que hace ruido. Las lineas importantes son las siguientes:

`import { useReducer } from 'react'`
Importamos el hook useReducer de la manera habitual

`import {initialState, reducer} from './formReducer'`
Importamos desde el archivo formReducer nuestro objeto initialState y la funcion reducer

`const [state, dispatch] = useReducer(reducer, initialState)`
Dentro del cuerpo de App, la utilización de nuestro useReducer es como se muestra en el código de arriba, como se ve, a useReducer le pasamos como parametro nuestra funcion reducer y nuestro objeto initialState, este hook nos devuelve el state actualizado y la funcion dispatch, con la cual vamos a mandar a ejecutar el action que necesitemos.

`dispatch({type:"ON_CHANGE", payload })`
Uso básico de dispatch. Type es el nombre del action a ejecutar, payload es el valor que se le pasa para que el action haga algo con ese valor, puede ser desde un string, number hasta un objeto o array. Por convención al objeto se le suele llamar payload.

`onChange={e => dispatch({type:"ON_CHANGE", payload: { nameProp: "apellido", value: e.target.value } }) }`

En este caso, nuestro onChange simplemente se limita a ejecutar una funcion dispatch que está compuesto por un objeto que tiene 2 propiedades, type y payload.
El objeto payload necesariamente debe tener la propiedad nameProp ya que de esta forma podremos actualizar el state con un solo action. Recordemos que nuestro action es de la siguiente forma:
```
case "ON_CHANGE":
  return {
    ...state,
    [action.payload.nameProp]: action.payload.value
  }
```

### Leer el state
Sin duda esto es lo mas facil de comprender, diría que este titulo es casi innecesario, pero para no dejar nada suelto paso a explicar.

Leer el state de tan facil como cuando se trata de un useState cualquiera. Con un console.log(state) podremos verlo facilmente, ni mas ni menos.

### Conclusión
El hook useReducer es bueno utilizarlo cuando se tiene un state medianamente grande en adelante, para separar la lógica. Además no es dificil de implementarlo ya que no se necesita instalar ninguna libreria adicional, ya viene en el core de React. Y por lo tanto no nos vemos en la necesidad de usar redux, evitando así todo lo que conlleva implementar esa librería y el uso correcto de la misma.
Personalmente, en el proyecto que estoy, debido a que tenemos modales con formularios grandes, useReducer nos viene como anillo al dedo, descomprimiendo los componentes de tanto código, mejorando la legibilidad. Sin duda, despues de useState vale la pena conocer useReducer.

*Y recuerda, los pandas sufren cuando ven states interminables con useState. Se buen programador, salva a los pandas, usa useReducer.*