Why the wheel
One obvious pain point of using forms in React is the need to maintain a large number of values and onchanges, such as a simple login box:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
username: "".password: ""
};
}
onUsernameChange = e= > {
this.setState({ username: e.target.value });
};
onPasswordChange = e= > {
this.setState({ password: e.target.value });
};
onSubmit = (a)= > {
const data = this.state;
// ...
};
render() {
const { username, password } = this.state;
return( <form onSubmit={this.onSubmit}> <input value={username} onChange={this.onUsernameChange} /> <input type="password" value={password} onChange={this.onPasswordChange} /> <button>Submit</button> </form> ); }}Copy the code
This is already a relatively simple login page, and some pages involve detailed editing, with more than ten or twenty components also common. Once there are many components, there are many disadvantages:
- Not easy to maintain: takes up a lot of space and hinders visibility.
- Performance may be affected:
setState
The use of can cause re-rendering if sub-components are not optimised, quite impacting performance. - Form verification: it is difficult to carry out form verification uniformly.
- .
To sum up, as a developer, we desperately want to have a form component that has both these features:
- Simple and easy to use
- The parent component can manipulate form data through code
- Avoid unnecessary component redrawing
- Support for custom components
- Support form verification
There are already many solutions in the form component community, such as React-final-Form, FormIK, Ant-Plus, noForm, etc., and many component libraries provide support in different ways, such as Ant-Design.
However, these solutions are more or less heavy, or still not easy to use, natural wheel is the best choice to meet the requirements.
How to make a wheel
The form component is implemented in three main parts:
Form
: Used to pass form context.Field
: form field component for automatic incomingvalue
andonChange
Go to the form component.FormStore
: Stores form data and encapsulates related operations.
To minimize the use of refs while still manipulating Form data (value, change value, manual validation, etc.), I separate the FormStore used to store the data from the Form component, create it with new FormStore(), and pass it in manually.
It might look something like this:
class App extends React.Component {
constructor(props) {
super(props);
this.store = new FormStore();
}
onSubmit = (a)= > {
const data = this.store.get();
// ...
};
render() {
return (
<Form store={this.store} onSubmit={this.onSubmit}>
<Field name="username">
<input />
</Field>
<Field name="password">
<input type="password" />
</Field>
<button>Submit</button>
</Form>); }}Copy the code
FormStore
Use to store form data, accept form initial values, and encapsulate actions on form data.
class FormStore {
constructor(defaultValues = {}, rules = {}) {
/ / form values
this.values = defaultValues;
// Form initial value, used to reset the form
this.defaultValues = deepCopy(defaultValues);
// Form validation rule
this.rules = rules;
// Event callback
this.listeners = []; }}Copy the code
To make form data changes responsive to form domain components, we use a subscription process to maintain a list of event callbacks in the FormStore. Each Field is created by calling formStore.subscribe (listener) to subscribe form data changes.
class FormStore {
// constructor ...
subscribe(listener) {
this.listeners.push(listener);
// Returns a function to unsubscribe
return (a)= > {
const index = this.listeners.indexOf(listener);
if (index > - 1) this.listeners.splice(index, 1);
};
}
// Call all listeners when the form changes
notify(name) {
this.listeners.forEach(listener= >listener(name)); }}Copy the code
Add get and set functions to get and set the form data. Here, notify(name) is called in the set function to ensure that notifications are triggered for all form changes.
class FormStore {
// constructor ...
// subscribe ...
// notify ...
// Get the form value
get(name) {
// If name is passed, the corresponding form value is returned, otherwise the whole form value is returned
return name === undefined ? this.values : this.values[name];
}
// Set the form value
set(name, value) {
// If name is specified
if (typeof name === "string") {
// Set the value of name
this.values[name] = value;
// Perform form validation, as shown below
this.validate(name);
// Notification form changes
this.notify(name);
}
// Set the form values in batches
else if (name) {
const values = name;
Object.keys(values).forEach(key= > this.set(key, values[key])); }}// Reset the form value
reset() {
// Clear error messages
this.errors = {};
// Reset the default value
this.values = deepCopy(this.defaultValues);
// Execute notification
this.notify("*"); }}Copy the code
For the form validation part, I don’t want to be too complicated, just make some rules
FormStore
Constructorrules
Is an object whose key corresponds to that of the form fieldname
And the value is oneCheck the function
.Check the function
Parameter takes the value of the form field and the entire form value, which is returnedboolean
orstring
Type result.
true
Indicates that the verification succeeds.false
andstring
Indicates that the verification fails andstring
The result represents an error message.
Then skillfully by | | symbol to judge whether to check through, such as:
new FormStore({/* Initial value */, {
username: (val) = >!!!!! val.trim() ||'User name cannot be empty'.password: (val) = >!!!!! (val.length >6 && val.length < 18) | |'Password must be more than 6 characters long and less than 18 characters long'.passwordAgain: (val, vals) = > val === vals.password || 'Inconsistent passwords entered twice'
}})
Copy the code
Implement a validate function in FormStore:
class FormStore {
// constructor ...
// subscribe ...
// notify ...
// get
// set
// reset
// Used to set and get error messages
error(name, value) {
const args = arguments;
// If no argument is passed, the first error message is returned
// const errors = store.error()
if (args.length === 0) return this.errors;
// If the name passed is of type number, error I is returned
// const error = store.error(0)
if (typeof name === "number") {
name = Object.keys(this.errors)[name];
}
// If value is passed, set or delete the error message corresponding to name according to value
if (args.length === 2) {
if (value === undefined) {
delete this.error[name];
} else {
this.errors[name] = value; }}// Return an error message
return this.errors[name];
}
// For form validation
validate(name) {
if (name === undefined) {
// Iterate over the entire form
Object.keys(this.rules).forEach(n= > this.validate(n));
// Notify the entire form of changes
this.notify("*");
// Returns an array containing the first error message and the form value
return [this.error(0), this.get()];
}
// Get the verification function according to name
const validator = this.rules[name];
// Get the form value according to name
const value = this.get(name);
// Execute the checksum function to get the result
const result = validator ? validator(name, this.values) : true;
// Get and set the error message in the result
const message = this.error(
name,
result === true ? undefined : result || ""
);
// Return the Error object or undefind, and the form value
const error = message === undefined ? undefined : new Error(message);
return[error, value]; }}Copy the code
Now that the Form component’s core FormStore is complete, it’s time to use it in the Form and Field components.
Form
The Form component is fairly simple, and is just there to provide an entry and delivery context.
The props receives an instance of FormStore and passes it through the Context to the child component (Field).
const FormStoreContext = React.createContext();
function Form(props) {
const { store, children, onSubmit } = props;
return (
<FormStoreContext.Provider value={store}>
<form onSubmit={onSubmit}>{children}</form>
</FormStoreContext.Provider>
);
}
Copy the code
Field
The Field component is also not complicated, and the core goal is to automatically pass value and onChange into the form component.
// Get the form value from the onChange event, which deals mainly with the checkbox special case
function getValueFromEvent(e) {
return e && e.target
? e.target.type === "checkbox"
? e.target.checked
: e.target.value
: e;
}
function Field(props) {
const { label, name, children } = props;
// Get the FormStore instance passed from Form
const store = React.useContext(FormStoreContext);
// The internal state of the component that triggers the re-rendering of the component
const [value, setValue] = React.useState(
name && store ? store.get(name) : undefined
);
const [error, setError] = React.useState(undefined);
// Form component onChange event, used to get the form value from the event
const onChange = React.useCallback(
(. args) = >name && store && store.set(name, valueGetter(... args)), [name, store] );// Subscription form data changes
React.useEffect((a)= > {
if(! name || ! store)return;
return store.subscribe(n= > {
// The current name data is changed, get the data and re-render
if (n === name || n === "*") { setValue(store.get(name)); setError(store.error(name)); }}); }, [name, store]);let child = children;
// If children is a valid component, pass in value and onChange
if (name && store && React.isValidElement(child)) {
const childProps = { value, onChange };
child = React.cloneElement(child, childProps);
}
// Form structure, the specific style is not posted
return (
<div className="form">
<label className="form__label">{label}</label>
<div className="form__content">
<div className="form__control">{child}</div>
<div className="form__message">{error}</div>
</div>
</div>
);
}
Copy the code
Now that the form component is complete, enjoy using it:
class App extends React.Component {
constructor(props) {
super(props);
this.store = new FormStore();
}
onSubmit = (a)= > {
const data = this.store.get();
// ...
};
render() {
return (
<Form store={this.store} onSubmit={this.onSubmit}>
<Field name="username">
<input />
</Field>
<Field name="password">
<input type="password" />
</Field>
<button>Submit</button>
</Form>); }}Copy the code
conclusion
This is just the core of the code, not as functional as the hundreds of star components, but simple enough to handle most of the project.
I’ve refined some of the details and released an NPM package, @react-hero/form, which you can install via NPM or find the source on Github. If you have any suggestions or suggestions, feel free to discuss them in the comments or issue.