Formularios de usuarios dinámicos con React Hooks

Esteban Aristizábal

Software Developer @ Kushki

noviembre 16, 2020

Lectura de 9 minutos

La creación de formularios con los cuales se captará la información de un usuario final puede ser una tarea difícil, debido a que se deben tomar en cuenta muchos factores como: validaciones en los campos, accesibilidad, mantenibilidad, que sea extensible y sujeto a cambios, lograrlo en el menor tiempo posible, con buenas prácticas, entre otros. Y puede ser más desafiante en el caso de que el formulario que deseemos desarrollar sea extenso, complejo, con numerosas secciones o que implique generar nuevos formularios según las elecciones del usuario.

En Kushki, llevamos a cabo este trabajo continuamente para ofrecer la mejor experiencia de usuario en el uso de nuestra plataforma, en este artículo te contaremos cómo logramos solucionar todas esas dificultades para un resultado óptimo en el desarrollo de formularios de usuario.

Formulario React Kushki

¿Cuáles son las tecnologías y librerías que se deben utilizar?

Es una librería para la construcción de interfaces de usuario, que principalmente ofrece la posibilidad de desarrollar complejas aplicaciones web con datos que cambian a través del tiempo. Se adapta muy bien a nuestras necesidades de captar constantemente información en nuestros distintos productos para actualizarlos en nuestra plataforma, además de presentar información que cambia continuamente de una forma inmediata y consistente. Algunas características muy importantes que también nos ofrece son: facilidad de uso, reusabilidad de componentes gráficos y eficiencia al construir interfaces complejas partiendo desde piezas más simples.

Son una nueva adición a React, permitiendo escribir aplicaciones con un enfoque funcional, reduciendo la cantidad de código necesario, simplificando los componentes complejos previamente desarrollados con clases a una estructura más simple la cual permite abstraer y reutilizar lógica en estas funciones para lograr componentes más fáciles de entender y mantener.

Es un lenguaje de programación que compila a Javascript, y la razón para utilizarlo es que Javascript no se pensó para la creación de grandes y complejos sistemas sino para la implementación de funcionalidades dinámicas a una página web; por eso Typescript se convierte en esa pieza fundamental para que Javascript sea altamente escalable manteniendo un gran nivel de flexibilidad. Al añadir un tipado estricto logramos un código más robusto y sólido, permitiendo que nuestras aplicaciones tengan menos errores, sean más fáciles de probar y una mayor mantenibilidad a través del tiempo, por eso nuestra adopción de este lenguaje de programación es muy alta en nuestro conjunto de aplicaciones.

Es una librería para la construcción de formularios con React, creada con el objetivo de lograr un mayor rendimiento y facilidad a la hora de implementar y validar los formularios.

Algunas razones por las cuales es recomendable utilizar React Hook Form son:

  • Es intuitiva, permitiendo al desarrollador utilizar una sola función encargada de abstraer toda la lógica del manejo del formulario.
  • Utiliza Hooks, consiguiendo un código más limpio al reutilizar funciones responsables del manejo de distintas partes de la interfaz.
  • Ofrece buen rendimiento, ya que minimiza el número de re-renderizaciones al utilizar Hooks y un aislamiento de los campos del formulario al momento que cambian su estado.
  • Es ligera, sin ninguna dependencia que afecte su tamaño al descargarla.
  • Posee validación incorporada; se encarga del control de errores que suceden al ingresar datos erróneos en los campos en el formulario.
  • Integración con Typescript, para mantener un tipado estricto de los campos que se deben captar del usuario.

Con algunas desventajas:

  • Requiere el uso de componentes funcionales, por lo que no es compatible con componentes realizados con clases.
  • El uso de Hooks al ser una característica nueva en React puede suponer un tiempo adicional para aprender a utilizarlos correctamente.

¿Cómo se debe implementar?

React Hook Form es la librería utilizada para la construcción del formulario, en esta sección nos centraremos en describir el proceso de desarrollo para llevarlo a cabo junto con explicaciones técnicas que permitan entender mejor cómo puede ser su adopción en un proyecto.

1. Define un tipado estricto del formulario

Al especificar una interfaz con los campos que tendrá el formulario nos permite mantener entendibilidad en el equipo de trabajo y brindar detección de errores en tiempo de compilación. A continuación tenemos una implementación de una interfaz de un formulario sencillo que captará información de un cliente.

interface IForm {
  clientName: string;
  clientDetails: {
    email: string;
    documentType: string;
    documentNumber: string;
  }
}

2. Inicializa el formulario con la función useForm()

Esta función retorna los métodos con los que se va a interactuar con la API de la librería, recibe como parámetro un tipado genérico con la interfaz que definimos anteriormente para mantener un tipado estricto del formulario y también especificamos que la validación de los campos sea con el modo “onBlur”, es decir al momento que el elemento pierde el enfoque del usuario.

 export const FormComponent: React.FC = () => {
   const form = useForm <IForm> ({
     mode: "onBlur",
   });
 }

3. Envuelve las secciones del formulario con el componente FormProvider

Este componente que ofrece la librería hace uso de la API Context de React, que resuelve el problema de pasar “props” en cada nivel del árbol de componentes, esto es específicamente útil en formularios complejos con múltiples secciones anidadas. Para la implementación hemos definido que el formulario tendrá 2 secciones anidadas, la primera captará el nombre del cliente y la segunda los detalles del cliente. Además de especificar la función “handleSubmitForm” que será la encargada de procesar los datos una vez que el usuario realice un envío del formulario.

export const FormComponent: React.FC = () => {
  const form = useForm<IForm>({
    mode: "onBlur",
  });

  const handleSubmitForm: SubmitHandler<IForm> = async (formData) => {
    // Guardar campos recibidos del formulario
  };

  return (
    <FormProvider {...form}>

      <ClientNameSection/>

      <ClientDetailsSection/>

      <Button
        style={{ margin: 20 }}
        variant="contained"
        color="primary"
        disableElevation
        onClick={form.handleSubmit(handleSubmitForm)}
      >
        Guardar
      </Button>
    </FormProvider>
  );
}; 

4. Utiliza el componente ConnectForm para secciones anidadas del formulario

Es muy común desarrollar formularios que tengan secciones que estén profundamente anidadas dentro del árbol de componentes, en este caso el componente “ConnectForm” se integra muy bien, permitiendo envolver al componente anidado con los métodos de la librería sin necesidad de obtenerlos de los “props”. Aquí hacemos uso de la técnica “renderProps” de React para reutilizar este componente en múltiples partes del código.

const ConnectForm = <T extends {}>({
  children,
}: {
  children: (form: UseFormMethods<T>) => JSX.Element;
}) => {
  const formMethods = useFormContext<T>();

  return children({
    ...formMethods,
  });
};

5. En las secciones anidadas del formulario utiliza el componente TypedController

Este componente de la librería se encargará de registrar el elemento en el estado del formulario para hacer un seguimiento de las entradas del usuario. Haremos uso del componente “Textfield” de la librería Material-UI el cual se envuelve dentro del TypedController en su atributo “render”. Se crearán 2 secciones anidadas “ClientNameSection” y “ClientDetailsSection”:

export const ClientNameSection: React.FC = () => {
  return (
    <ConnectForm<IForm>>
      {({ control, errors }) => {
        const TypedController = useTypedController<IForm>({
          control: control,
        });

        return (
          <div style={{ padding: 20 }}>
            <TypedController
              name={"clientName"}
              rules={{ required: true }}
              render={(props) => (
                <TextField
                  {...props}
                  label="Nombre del cliente"
                  variant="outlined"
                  error={!!errors.clientName}
                  required
                  fullWidth
                  margin="normal"
                  helperText={
                    !!errors.clientName &&
                    "Campo requerido"
                  }
                />
              )}
            />
          </div>
        );
      }}
    </ConnectForm>
  );
};
export const ClientDetailsSection: React.FC = () => {
  return (
    <ConnectForm<IForm>>
      {({ control, errors }) => {
        const TypedController = useTypedController<IForm>({
          control: control,
        });

        return (
          <div style={{ padding: 20 }}>
            <div>
              <Typography variant="h6" color="primary">
                Detalles del cliente
              </Typography>
              <TypedController
                name={["clientDetails", "email"]}
                rules={{ required: true, pattern: emailPattern }}
                render={(props) => (
                  <TextField
                    {...props}
                    label="Correo electrónico"
                    variant="outlined"
                    error={!!errors.clientDetails?.email}
                    required
                    fullWidth
                    margin="normal"
                    helperText={
                      !!errors.clientDetails?.email &&
                      "Email inválido"
                    }
                  />
                )}
              />
            </div>
            <div style={{ display: "flex" }}>
              <div style={{ width: "50%", marginRight: 10 }}>
                <FormControl
                  variant="outlined"
                  fullWidth
                  margin="normal"
                >
                  <InputLabel>Tipo de documento</InputLabel>
                  <TypedController
                    name={["clientDetails", "documentType"]}
                    defaultValue={"CI"}
                    render={(props) => (
                      <Select {...props} label="Tipo de documento">
                        <MenuItem key="CI" value="CI">
                          {"CI"}
                        </MenuItem>
                        <MenuItem key="RUC" value="RUC">
                          {"RUC"}
                        </MenuItem>
                        <MenuItem key="PAS" value="PAS">
                          {"PAS"}
                        </MenuItem>
                      </Select>
                    )}
                  />
                </FormControl>
              </div>
              <div style={{ width: "50%", marginLeft: 10 }}>
                <TypedController
                  name={["clientDetails", "documentNumber"]}
                  render={(props) => (
                    <TextField
                      {...props}
                      id="documentNumber"
                      label="Número de documento"
                      variant="outlined"
                      error={!!errors.clientDetails?.documentNumber}
                      fullWidth
                      margin="normal"
                    />
                  )}
                />
              </div>
            </div>
          </div>
        );
      }}
    </ConnectForm>
  );
}; 

Además el componente “TypedController” mantiene un tipado estricto del formulario como observamos en la siguiente sección de código, al colocar un argumento equivocado en el atributo “name”, el compilador detecta un error en tiempo de ejecución.

Podemos ver cómo se visualizaría en el navegador con las 2 secciones anidadas del formulario.

6. Observar los cambios en los campos del formulario con la función useWatch()

En la fase de desarrollo es frecuente la necesidad de realizar acciones de acuerdo a las entradas del usuario, por ejemplo, para renderizar condicionalmente o para validar datos en tiempo real, la función “useWatch()” nos permite estar escuchando los cambios de un campo del formulario y actuar de acuerdo a este. En nuestra implementación, se puede utilizar la función para validar si el campo “email” existe al momento que el usuario lo ingresa en el formulario:

export const ClientDetailsSection: React.FC = () => {
  return (
    <ConnectForm<IForm>>
      {({ control, errors }) => {
        const TypedController = useTypedController<IForm>({
          control: control,
        });

        const email = useWatch({ name: "email" }) as string;

        useEffect(() => {
          // Verificar si email ya existe
        }, [email]);

        ...         

7. Enviar los valores del formulario con la función handleSubmit()

Esta función pasará la información recopilada una vez que se realice una validación exitosa para poder guardar los datos del formulario.

export const FormComponent: React.FC = () => {
    const form = useForm < IForm > ({
      mode: "onBlur",
    });

    const handleSubmitForm: SubmitHandler < IForm > = async (formData) => {

      // Guardar campos recibidos del formulario  
    };

    ...

Previamente a la invocación de la función se realiza una validación de los campos, como podemos observar en la siguiente imagen la librería se encarga de detectar los errores y actualizarlos en la interfaz.

Por último, con los campos completos podemos ver cómo se recibirán en la función para poder procesarlos:

 {
  "clientName": "John Doe",
  "clientDetails": {
    "email": "jhon.doe@test.com",
    "documentType": "CI",
    "documentNumber": "1764537289"
  }
}

¿Cuales son nuestros resultados?

Actualmente en Kushki, esta implementación nos ha permitido:

  • Construir formularios de una forma más eficiente reduciendo el código escrito.
  • Formularios extensos y complejos se los desarrolla a partir de componentes simples y se delega a la librería la responsabilidad de validarlo y controlarlo.
  • Contar con una estandarización en la forma de desarrollarlos para lograr una mejor mantenibilidad del código a lo largo del tiempo dentro del equipo de trabajo.
  • Lograr mejoras de rendimiento al reducir al mínimo el número de renderizaciones que debe realizar el navegador.
  • Detectar errores antes de que el código esté en producción gracias al tipado estricto del formulario.

También, es importante tomar en cuenta estas observaciones antes de adaptarlo en un proyecto.

  • Existirá un tiempo adicional de aprendizaje hasta que el equipo de trabajo implemente de forma adecuada la librería.
  • Puede ser una solución excesiva si el formulario que se va a desarrollar es sencillo con pocos elementos.
  • La librería cuenta con menos comunidad y colaboradores que otras librerías más antiguas en el mercado como son Formik o Redux Form. Puedes ver una completa comparación con estas librerías en este enlace.

Finalmente, espero que este artículo haya sido de ayuda en el proyecto que estés llevando a cabo o planees realizarlo, y puedas adoptar esta forma de construir formularios para que tengas un proceso de desarrollo óptimo y eficiente.

¿Te gustaría mantenerte al tanto de nuestro contenido? Suscríbete a nuestra lista de correos.