Working With Forms in React.js Using The Basic Toolkit
A tutorial on using React.js with its built-in toolkit and how to use the concept of High Order Components in your JavaScript code.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
During my work with React.js, I often had to deal with the processing of forms. I tried using Redux-Form, React-Redux-Form, but none of the libraries suited all my needs. I did not like that the state of the form is stored in the reducer, and each event passes through the action creator. Also, according to Dan Abramov, "the state of the form is inherently ephemeral and local, so you do not need to track it in Redux (or any Flux library)."
I note that React-Redux-Form has a LocalForm
component that allows developers to work without redux, but, in my opinion, it's useless to install a 21.9KB library and use it for less than half the time.
I'm not against these libraries, in specific cases they are irreplaceable. For example, when the third-party component that is not associated with the form is dependent on the data entered. But, in my article, I want to consider forms that do not need Redux.
I started using the local state component, and new difficulties arose: the amount of code increased, the components lost readability, and a lot of duplication appeared.
The solution to the problems was the concept of High Order Components. Briefly, HOC is a function that receives a component on the input and returns it, updated, with the integration of additional or changed props. More information about HOC can be found on the official website of React.js. The purpose of using the concept of HOC was to split the component into two parts, one of which would be responsible for the logic, and the second - for the mapping.
Creating a Form
As an example, we will create a simple feedback form, in which there will be 3 fields: name, email, phone.
For simplicity, use the Create-React-App. Establish it globally:
npm i -g create-react-app
Then create your application in a clean form folder:
create-react-app pure-form
In addition, we set prop-types and class names, as they will be useful in the future:
npm i prop-types classnames -S
Create two folders: /components and /containers. The /components folder will contain all the components that are responsible for the mapping. In the /containers folder, the components responsible for the logic.
In the /components folder, create the file Input.jsx, in which we declare the common component for all the intuitions. It is important at this stage not to forget to qualitatively register ProptTypes
and defaultProps
, to provide the ability to add custom classes, and also to inherit it from PureComponent
for optimization.
The result is:
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;
Next, in the /components folder, create the file Form.jsx, in which the composition containing the form will be declared. All methods for working with it will be obtained through props, as well as the value for the intuitions, so the state is not needed here. We get:
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 = "Login"
error = {errors.username}
required
/>
<Input
key = "phone"
value = {phone}
name = "phone"
onChange = {handleInput}
placeholder = "Phone"
error = {errors.phone}
required
/>
<Input
key = "email"
value = {email}
type = "email"
name = "email"
onChange = {handleInput}
placeholder = "Email"
error = {errors.email}
required
/>
<button type = "submit" className = "submitBtn">
Submit form
</ 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);
Creating an HOC
In the /containers folder, create the file FormWrapper.jsx. We declare a function that takes the WrappedComponent
component as its argument and returns the WrappedForm
class. The render method of this class returns a WrappedComponent
with props integrated into it. Try to use the classic function declaration, as this will simplify the debugging process.
In the WrappedForm
class, we create a state: isFetching
. This will act as a flag to control asynchronous requests, data (an object with a value of the intuitions), and errors (an object for storing errors). The declared state will be passed to WrappedComponent
. In this way, the storage of the form state storage is carried out to the upper level, which makes the code more readable and transparent.
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} />;
}
};
}
But such an implementation is not universal, because for each form you will have to create your own wrapper. You can improve this system and put HOC inside one more function that will generate initial state values.
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} />;
}
};
};
}
In this function, you can pass not only the initial state values, but, generally, any parameters. For example, the attributes and methods on which you can create a form in Form.jsx. An example of such an implementation will be the topic for the next article.
In the Form.jsx file, declare the initial state values and pass them to the HOC:
const initialState = {
username: '',
phone: '',
email: '',
};
export default FormWrapper(initialState, initialState)(Form);
Let's create a handleInput
method for processing the values entered into the input. It receives an event, from which we take value
and name
and pass them to setState
. Since the values of the intuitions are stored in the data object, in setState
we call the function. Simultaneous to saving the received value, we zero the error store of the field being changed. We get:
handleInput = event => {
const { value, name } = event.currentTarget;
this.setState(({ data, errors }) => ({
data: {
...data,
[name]: value,
},
errors: {
...errors,
[name]: '',
},
}));
};
Now create a handeSubmit
method to process the form and output the data to the console, but, before that, you need to pass validation. We will only validate the required fields, that is, all the keys of this.state.errors
. We get:
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);
}
};
Use the reduce
method to list all the required fields. At each iteration, the validate method is invoked, to which we pass the name/value pair. Inside the method, the correctness of the entered data is checked, which results in the return of the boolean type. If at least one pair of values fails validation, the variable isValid
will be false, and the data in the console will not be displayed, that is, the form will not be processed. Here we consider the simple case of checking for a nonempty form. Here's the validate
method:
validate = (name, value) => {
if (! value.trim ()) {
this.setState (
({errors}) => ({
errors: {
... errors,
[name]: 'field must not be empty',
},
}),
() => false
);
} else {
return true;
}
};};
Both the handleSubmit
and the handleInput
methods must be passed to WrappedComponent
:
render() {
return (
<WrappedComponent
{...this.state}
{...this.props}
handleInput={this.handleInput}
handleSubmit={this.handleSubmit}
/>
);
}
As a result, we get a ready-made feedback form, with simple validation and error reporting. In doing so, we have removed the logical part from the component responsible for the mapping.
Conclusion
So, we have considered a basic example of creating an HOC for form processing. When creating the form, only simple intuitions were used, without complex elements, such as drop-down lists, checkboxes, radio buttons, and others. If they are available, you may have to create additional methods for handling events.
Opinions expressed by DZone contributors are their own.
Comments