Introduccion
Mientras trabajaba en React.js, a menudo tenía que lidiar con el procesamiento de formularios. Redux-Form , React-Redux-Form pasó por mis manos, pero ninguna de las bibliotecas me satisfizo por completo. No me gustó que el estado del formulario se almacena en el reductor , y cada evento pasa a través del creador de la acción . Además, según Dan Abramov, "el estado del formulario es intrínsecamente efímero y local, por lo que no es necesario realizar un seguimiento en Redux (o en cualquier biblioteca de Flux)".
Observo que en React-Redux-Form hay un componente LocalForm que le permite trabajar sin redux, pero en mi opinión, no tiene sentido instalar una biblioteca de 21.9kB y usarla menos de la mitad.
No estoy en contra de las bibliotecas nombradas, en casos específicos son irremplazables. Por ejemplo, cuando un componente de terceros no relacionado con el formulario depende de los datos ingresados. Pero en mi artículo quiero considerar formularios que no necesitan redux.
Comencé a usar el estado local del componente, y surgieron nuevas dificultades: la cantidad de código aumentó, los componentes perdieron legibilidad, aparecieron muchas duplicaciones.
La solución fue el concepto de Componente de alto orden. En resumen, HOC es una función que recibe una entrada de componente y la devuelve actualizada con la integración de accesorios adicionales o modificados. Lea más sobre HOC en el sitio web oficial de React.js . El propósito de utilizar el concepto HOC era dividir el componente en dos partes, una de las cuales sería responsable de la lógica y la segunda, de la pantalla.
Creación de formularios
Como ejemplo, crearemos un formulario de comentarios simple en el que habrá 3 campos: nombre, correo electrónico, teléfono.
Para simplificar, usamos Create-React-App . Instalarlo globalmente:
npm i -g create-react-app
luego cree su aplicación en la carpeta de forma pura
create-react-app pure-form
Además, instale prop-types y nombres de clase , nos serán útiles en el futuro:
npm i prop-types classnames -S
Cree dos carpetas / componentes y / contenedores . La carpeta / components contendrá todos los componentes responsables de la visualización. En la carpeta / container , los componentes responsables de la lógica.
En la carpeta / components , cree un archivo Input.jsx en el que declaremos un componente común para todas las entradas. Es importante en esta etapa no olvidar prescribir ProptTypes y defaultProps de manera de calidad, proporcionar la posibilidad de agregar clases personalizadas y también heredarlo de PureComponent para su optimización.
El resultado es:
import React, { PureComponent } from 'react'; import cx from 'classnames'; import PropTypes from 'prop-types'; class Input extends PureComponent { render() { const { name, error, labelClass, inputClass, placeholder, ...props } = this.props; return ( <label className={cx('label', !!labelClass && labelClass)} htmlFor={`id-${name}`} > <span className="span">{placeholder}</span> <input className={cx( 'input', !!inputClass && inputClass, !!error && 'error' )} name={name} id={`id-${name}`} onFocus={this.handleFocus} onBlur={this.handleBlur} {...props} /> {!!error && <span className="errorText">{error}</span>} </label> ); } } Input.defaultProps = { type: 'text', error: '', required: false, autoComplete: 'off', labelClass: '', inputClass: '', }; Input.propTypes = { value: PropTypes.string.isRequired, name: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, placeholder: PropTypes.string.isRequired, error: PropTypes.string, type: PropTypes.string, required: PropTypes.bool, autoComplete: PropTypes.string, labelClass: PropTypes.string, inputClass: PropTypes.string, }; export default Input;
A continuación, en la carpeta / components , cree el archivo Form.jsx , en el que se declarará el componente que contiene el formulario. Recibiremos todos los métodos para trabajar con él a través de accesorios, así como el valor de las entradas, por lo que el estado no es necesario aquí. Obtenemos:
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Input from './Input'; import FormWrapper from '../containers/FormWrapper'; class Form extends Component { render() { const { data: { username, email, phone }, errors, handleInput, handleSubmit, } = this.props; return ( <div className="openBill"> <form className="openBillForm" onSubmit={handleSubmit}> <Input key="username" value={username} name="username" onChange={handleInput} placeholder="" error={errors.username} required /> <Input key="phone" value={phone} name="phone" onChange={handleInput} placeholder="" error={errors.phone} required /> <Input key="email" value={email} type="email" name="email" onChange={handleInput} placeholder=" " error={errors.email} required /> <button type="submit" className="submitBtn"> </button> </form> </div> ); } } Form.propTypes = { data: PropTypes.shape({ username: PropTypes.string.isRequired, phone: PropTypes.string.isRequired, email: PropTypes.string.isRequired, }).isRequired, errors: PropTypes.shape({ username: PropTypes.string.isRequired, phone: PropTypes.string.isRequired, email: PropTypes.string.isRequired, }).isRequired, handleInput: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, }; export default FormWrapper(Form);
Creación de HOC
En la carpeta / container , cree el archivo FormWrapper.jsx . Declaramos la función dentro, que toma el componente WrappedComponent como argumento y devuelve la clase WrappedForm . El método de renderizado de esta clase devuelve un WrappedComponent con accesorios integrados en él. Intente utilizar la declaración de función clásica, esto simplificará el proceso de depuración.
En la clase WrappedForm, cree el estado : isFetching : un indicador para controlar solicitudes asincrónicas, datos , un objeto con valores de entrada, errores , un objeto para almacenar errores. El estado declarado se pasa al WrappedComponent . Por lo tanto, se implementa la implementación del almacenamiento de estados de formulario en el nivel superior, lo que hace que el código sea más legible y transparente.
export default function Wrapper(WrappedComponent) { return class FormWrapper extends Component { state = { isFetching: false, data: { username: '', phone: '', email: '', }, errors: { username: '', phone: '', email: '', }, }; render() { return <WrappedComponent {...this.state} />; } }; }
Pero tal implementación no es universal, porque para cada formulario debe crear su propio contenedor. Puede mejorar este sistema e incrustar el HOC dentro de otra función que formará los valores de estado iniciales.
import React, { Component } from 'react'; export default function getDefaultValues(initialState, requiredFields) { return function Wrapper(WrappedComponent) { return class WrappedForm extends Component { state = { isFetching: false, data: initialState, errors: requiredFields, }; render() { return <WrappedComponent {...this.state} {...this.props} />; } }; }; }
En esta función, puede pasar no solo los valores iniciales de estado , sino generalmente cualquier parámetro. Por ejemplo, atributos y métodos en función de los cuales será posible crear un formulario en Form.jsx . Un ejemplo de tal implementación será el tema para el próximo artículo.
En el archivo Form.jsx, declaramos los valores de estado iniciales y los pasamos al HOC:
const initialState = { username: '', phone: '', email: '', }; export default FormWrapper(initialState, initialState)(Form);
Creemos el método handleInput para procesar los valores ingresados en la entrada. Recibe un evento del que tomamos valor y nombre y los transferimos a setState . Como los valores de entrada se almacenan en el objeto de datos , llamamos a la función en setState . Simultáneamente al guardar el valor obtenido, ponemos a cero el error de almacenamiento del campo variable. Obtenemos:
handleInput = event => { const { value, name } = event.currentTarget; this.setState(({ data, errors }) => ({ data: { ...data, [name]: value, }, errors: { ...errors, [name]: '', }, })); };
Ahora crearemos el método handeSubmit para procesar el formulario y mostrar los datos en la consola, pero antes de eso debemos pasar la validación. Validaremos solo los campos obligatorios, es decir, todas las claves del objeto this.state.errors. Obtenemos:
handleSubmit = e => { e.preventDefault(); const { data } = this.state; const isValid = Object.keys(data).reduce( (sum, item) => sum && this.validate(item, data[item]), true ); if (isValid) { console.log(data); } };
Usando el método reduce, clasificamos todos los campos requeridos. En cada iteración, se llama al método de validación , en el que pasamos un par de nombre , valor . Dentro del método, se realiza una verificación de la exactitud de los datos ingresados, cuyos resultados devuelven el tipo booleano. Si al menos un par de valores no pasa la validación, la variable isValid se convertirá en falsa y los datos no se enviarán a la consola, es decir, el formulario no se procesará. Aquí se considera un caso simple: un cheque para un formulario no vacío. Validar método:
validate = (name, value) => { if (!value.trim()) { this.setState( ({ errors }) => ({ errors: { ...errors, [name]: ' ', }, }), () => false ); } else { return true; } };
Los métodos handleSubmit y handleInput deben pasarse al componente WrappedComponent :
render() { return ( <WrappedComponent {...this.state} {...this.props} handleInput={this.handleInput} handleSubmit={this.handleSubmit} /> ); }
Como resultado, obtenemos un formulario de comentarios listo para usar, con validación simple y salida de error. Al mismo tiempo, eliminamos la parte lógica del componente responsable de la pantalla.
Conclusión
Entonces, vimos un ejemplo básico de creación de un HOC para procesar formularios. Al crear el formulario, solo se utilizaron entradas simples, sin elementos complejos, como listas desplegables, casillas de verificación, botones de opción y otros. Si está disponible, es posible que deba crear métodos de procesamiento de eventos adicionales.
Escribir preguntas y comentarios en los comentarios al artículo o a mi correo.
Un ejemplo completo se puede encontrar aquí: forma de reacción pura .
Gracias por su atencion!