React es una herramienta muy poderosa que aún luego de varios años de uso puedes seguir aprendiendo formas más eficientes de realizar las mismas tareas. Esto me quedó muy claro hace algunas semanas cuando me vi en la necesidad de hacer una pequeña actualización a un formulario que había hecho hace ya varios años. Si bien el formulario había estado funcionando sin problemas aparentes, al ver el código que había escrito hace ya varias lunas me di cuenta inmediatamente de un pequeño detalle que ahora me parece muy evidente pero en su momento definitivamente no era el caso.

Estoy seguro que para muchos es de lo más normal un componente escrito de la siguiente manera:

import React, { Component } from 'react';

class Componente extends Component {
  constructor(props) {
    super(props);
  
    this.state {
      campoUno: '',
      campoDos: '',
      campoTres: ''
      // otras variables 
    };
  
    this.actualizarCampoUno = this.actualizarCampoUno.bind(this);
    this.actualizarCampoDos = this.actualizarCampoDos.bind(this);
    this.actualizarCampoTres = this.actualizarCampoTres.bind(this);
  };
  
  actualizarCampoUno = (e, { value }) => {
    this.setState({ 
      campoUno: value
    });  
  };
  
  actualizarCampoDos = (e, { value }) => {
      this.setState({
        campoDos: value
      });
  };
  
  actualizarCampoTres = (e, { value }) => {
      this.setState({
        campoTres: value
      });
  };
  
  render() {
    return (
      <div>
        Campo Uno
        <input 
          onChange={this.actualizarCampoUno}
          value={this.state.campoUno} />
        <br />
          Campo Dos
        <input 
          onChange={this.actualizarCampoDos}
          value={this.state.campoDos} />
        <br />
          Campo Tres
        <input 
          onChange={this.actualizarCampoTres}
          value={this.state.campoTres} />
        <br />
      </div>
    );
  };  
};

export default Componente;

Para un nivel de tutorial este componente es perfectamente válido. Este es el tipo de React con el que aprendí y que escribí por mucho tiempo. Sin embargo una de las tristes realidades que se vuelven evidentes luego de muchas batallas es que el rendimiento de React se ve severamente impactado cuando tus aplicativos crecen en complejidad y componentes escritos de esta manera contribuyen a degradar aún más la experiencia del usuario.

Optimizar este bloque de código es relativamente fácil y no sólo nos ahorra una cantidad considerable de funciones si nuestro componente tiene muchos inputs, también es la clave para poder implementar interfaces de usuario dinámicas donde necesitamos crear controles al vuelo de acuerdo a la interacción del usuario (tema para otro día). Empecemos por lo más obvio: estamos declarando una función que actualiza una variable específica del estado del componente. Idealmente queremos una función como ésta:

actualizarCampo = (campo, valor) => {
  this.setState({ campo: valor });
};

Por supuesto, el primer obstáculo es que deseamos ser específicos acerca de la variable de estado que queremos actualizar. Y adicicionalmente queremos que nuestros inputs puedan informar a la función el campo específico que estamos actualizando. Tenemos dos formas de hacerlo, la primera es viable pero no es tan elegante pero igual la describo a continuación por simplicidad:

actualizarCampo = (campo, valor) => {
  this.setState({ [campo]: valor });
};

// Dentro del render
<input 
  onChange={e => this.actualizarCampo('campoUno', e.target.value) }
  value={this.state.campoUno} />

He hecho una pequeña pero significante modificación a nuestra función de actualizar campo. En la línea dos podemos ver que cambiamos campo por [campo]. Este pequeño detalle es significativo porque aunque recibamos el nombre del campo como un parámetro de tipo string en esta función, al encerrarlo entre corchetes React ya no lo evalúa como string sino busca dentro del estado del componente una variable que coincida con el string que estamos suministrando. Esto hace entonces que la actualización ya no sea con funciones estáticas por variable sino que permite entonces invocar esta función desde cualquier input o algún otro elemento e indicarle de forma dinámica la variable que deseamos actualizar.

La segunda forma toma esta misma idea pero en vez de declarar una función dentro del evento onChange del elemento utilizaremos un atributo del mismo elemento:

actualizarCampo = (e, { name, value}) => {
  this.setState({ [name]: value });
};

// Dentro del render
<input 
    name="campoUno"
    onChange={this.actualizarCampo}
    value={this.state.campoUno} />

Acá nos interesa ver lo que está sucediendo con nuestros parámetros en la línea 1, y las líneas 7 y 8 ya dentro del input. He modificado nuevamente la forma en que recibimos los parámetros y estoy utilizando ya los atributos name y value estándar del elemento input. Debido a que en la línea 8 estoy únicamente haciendo referencia a mi función actualizarCampo ya no tengo necesidad de declarar acá una función adicional que pasa explícitamente los parámetros que le estoy suministrando a la función que se encarga de actualizar mi estado. En la línea 7 he tenido el cuidado de declarar el atributo name con el nombre de la variable de estado de la que tomará su valor y ya mi función en la línea 1 toma únicamente los atributos name y value que son los que realmente me interesan.

¿Y si tengo un input que necesita validarse antes de actualizar el estado?

Es común que tengamos componentes que manejan valores distintos a un simple string. Usualmente un calendario que maneja fechas o incluso un input que maneja valores de tipo numérico; podemos con un poco de imaginación mantener siempre una misma función que maneje todos estos tipos de datos sin necesidad de recurrir a distintas funciones para distintos tipos de datos. La forma más sencilla que he encontrado es introducir un switch dentro de nuestra función de actualización que evalúa el tipo de datos que estamos manejando, siendo necesario entonces agregar un atributo a nuestros inputs.

actualizarCampo = (e, { fieldType, name, value }) => {
  switch (fieldType) {
    case 'float':
      this.setState({ [name]: parseFloat(value) });
    break;  
    case 'date':
      this.setState({ [name]: new Date(value) });
    default: 
      this.setState({ [name]: value });
    break;
  }
};

// Dentro del render
<input 
  name="campoCuatro"
  fieldType="string"
  onChange={this.actualizarCampo}
  value={this.state.campoUno} />

<input 
  name="campoCuatro"
  fieldType="float"
  onChange={this.actualizarCampo}
  value={this.state.campoUno} />

<DatePicker
    onChange={(e) => { this.actualizarCampo(e, { fieldType: 'date', name: 'campoCinco', value: e }) }}
    selected={this.state.campoCinco} />

Valiéndonos de la practicidad del default de nuestro switch, dejamos dicho rebalse para el caso más común, usualmente los campos de tipo string y declaramos el manejo para casos especiales de forma aparte. En este ejemplo estoy utilizando un input que necesita ser parseado como tipo float y un control de tipo DatePicker que entrega una fecha y por lo tanto debe ser parseada como tal.

A veces menos es más

Como podemos ver, a veces necesitamos hacer menos para lograr más. En un framework tan complejo como lo puede llegar a ser React es muy importante que podamos optimizar en algo tan sencillo como ahorrarnos 20 funciones para igual cantidad de campos que tiene nuestra interfaz de usuario. La reutilización de código es también un principio básico que se beneficia de un approach como el acá descrito.

Regresando a mi introducción, lo que se suponía que fuera una actualización trivial terminó siendo una semi-reescritura del formulario que iba a modificar. Lo que debía ser una actualización de 5 minutos terminó siendo una pequeña desviación de 2 horas pero el rendimiento y legibilidad de mi código mejoraron enormemente.