1. Introdução
Enquanto trabalhava no React.js, muitas vezes tive que lidar com o processamento de formulários. Forma Redux, Forma React-Redux passou por minhas mãos, mas nenhuma das bibliotecas me satisfez completamente. Não gostei que o estado do formulário seja armazenado no redutor e cada evento passe pelo criador da ação . Além disso, de acordo com Dan Abramov, "o estado do formulário é inerentemente efêmero e local, portanto você não precisa rastreá-lo no Redux (ou em qualquer biblioteca do Flux)".
Observo que no React-Redux-Form existe um componente LocalForm que permite trabalhar sem redux, mas, na minha opinião, não faz sentido instalar uma biblioteca de 21,9kB e usá-la menos da metade.
Não sou contra as bibliotecas nomeadas; em casos específicos, elas são insubstituíveis. Por exemplo, quando um componente de terceiros não relacionado ao formulário depende dos dados inseridos. Mas no meu artigo, quero considerar formas que não precisam de redux.
Comecei a usar o estado local do componente e surgiram novas dificuldades: a quantidade de código aumentou, os componentes perderam a legibilidade, apareceram muitas duplicações.
A solução foi o conceito de componente de alta ordem. Em resumo, o HOC é uma função que recebe uma entrada de componente e a retorna atualizada com a integração de adereços adicionais ou modificados. Leia mais sobre o HOC no site oficial do React.js . O objetivo de usar o conceito HOC era dividir o componente em duas partes, uma das quais seria responsável pela lógica e a segunda - pela exibição.
Criação de formulário
Como exemplo, criaremos um formulário de feedback simples, no qual haverá três campos: nome, email, telefone.
Para simplificar, usamos o Create-React-App . Instale-o globalmente:
npm i -g create-react-app
em seguida, crie seu aplicativo na pasta pura
create-react-app pure-form
Além disso, instale prop-types e classnames , eles serão úteis para nós no futuro:
npm i prop-types classnames -S
Crie duas pastas / componentes e / recipientes . A pasta / components conterá todos os componentes responsáveis pela exibição. Na pasta / containers , os componentes responsáveis pela lógica.
Na pasta / components , crie um arquivo Input.jsx no qual declaramos um componente comum para todas as entradas. É importante, neste estágio, não esquecer de prescrever ProptTypes e defaultProps de maneira de qualidade, prever a possibilidade de adicionar classes personalizadas e também herdá-lo do PureComponent para otimização.
O resultado é:
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;
Em seguida, na pasta / components , crie o arquivo Form.jsx , no qual o componente que contém o formulário será declarado. Receberemos todos os métodos para trabalhar com ele por meio de adereços, além de valor para entradas, portanto, o estado não é necessário aqui. Temos:
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);
Criação de HOC
Na pasta / containers , crie o arquivo FormWrapper.jsx . Declaramos a função inside, que pega o componente WrappedComponent como argumento e retorna a classe WrappedForm . O método de renderização dessa classe retorna um WrappedComponent com objetos integrados. Tente usar a declaração de função clássica, isso simplificará o processo de depuração.
Na classe WrappedForm, crie state : isFetching - um sinalizador para controlar solicitações assíncronas, dados - um objeto com valores de entrada, erros - um objeto para armazenar erros. O estado declarado é passado para o WrappedComponent . Assim, é implementada a implementação do armazenamento dos estados do formulário no nível superior, o que torna o código mais legível e 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} />; } }; }
Mas essa implementação não é universal, porque para cada formulário você precisa criar seu próprio wrapper. Você pode melhorar esse sistema e incorporar o HOC dentro de outra função que formará os valores iniciais do estado.
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} />; } }; }; }
Nesta função, você pode passar não apenas os valores iniciais do estado , mas geralmente quaisquer parâmetros. Por exemplo, atributos e métodos com base nos quais será possível criar um formulário no Form.jsx . Um exemplo dessa implementação será o tópico do próximo artigo.
No arquivo Form.jsx, declaramos os valores iniciais do estado e os passamos para o HOC:
const initialState = { username: '', phone: '', email: '', }; export default FormWrapper(initialState, initialState)(Form);
Vamos criar o método handleInput para processar os valores inseridos na entrada. Ele recebe um evento do qual assumimos valor e nome e os transferimos para setState . Como os valores de entrada são armazenados no objeto de dados , chamamos a função em setState . Simultaneamente, ao salvar o valor obtido, zeramos o armazenamento de erros do campo variável. Temos:
handleInput = event => { const { value, name } = event.currentTarget; this.setState(({ data, errors }) => ({ data: { ...data, [name]: value, }, errors: { ...errors, [name]: '', }, })); };
Agora vamos criar o método handeSubmit para processar o formulário e exibir os dados no console, mas antes disso precisamos passar na validação. Validaremos apenas os campos obrigatórios, ou seja, todas as chaves do objeto this.state.errors. Temos:
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 o método de redução, classificamos todos os campos obrigatórios. A cada iteração, o método validate é chamado, no qual passamos um par de nome , valor . Dentro do método, é feita uma verificação da correção dos dados inseridos, cujos resultados retornam o tipo Booleano. Se pelo menos um par de valores não passar na validação, a variável isValid se tornará falsa e os dados não serão enviados para o console, ou seja, o formulário não será processado. Um caso simples é considerado aqui - uma verificação de um formulário não vazio. Validar método:
validate = (name, value) => { if (!value.trim()) { this.setState( ({ errors }) => ({ errors: { ...errors, [name]: ' ', }, }), () => false ); } else { return true; } };
Os métodos handleSubmit e handleInput devem ser passados para o WrappedComponent :
render() { return ( <WrappedComponent {...this.state} {...this.props} handleInput={this.handleInput} handleSubmit={this.handleSubmit} /> ); }
Como resultado, obtemos um formulário de feedback pronto, com validação simples e saída de erro. Ao mesmo tempo, removemos a parte lógica do componente responsável pela exibição.
Conclusão
Então, vimos um exemplo básico de criação de um HOC para processar formulários. Ao criar o formulário, foram usadas apenas entradas simples, sem elementos complexos, como listas suspensas, caixas de seleção, botões de opção e outros. Se disponível, pode ser necessário criar métodos adicionais de processamento de eventos.
Escreva perguntas e comentários nos comentários para o artigo ou para o meu e-mail.
Um exemplo completo pode ser encontrado aqui: forma de reação pura .
Obrigado pela atenção!