As we saw in the first part of the article, forms are a very important topic. Since they are extremely common in the applications we build. In this article, we will continue learning how to create forms using components controlled by React. And we will see how to build generic components to help us minimize and optimize the code. After creating the context in FormGeneric and the context provider, it is time to explain how to consume the context of FormGeneric.
After the points seen in the previous article, now it’s time to focus on who consumes FormGeneric, FieldGeneric:
import { FC, useContext, ChangeEvent } from 'react';
import { FormContext } from './FormGeneric';
<FormContext.Consumer>
{({ values, errors }) => (
<div>
...
{(type === 'Text') && (
<input
type={type.toLowerCase()}
id={name}
value={
values[name] === undefined ? '' : values[name]
}
onChange={handleChange}
onBlur={handleBlur}
/>
)}
{type === 'TextArea' && (
...
In the image below, we show a generic form as well as any component, in this case, ArticleUsingFormGeneric, we use FormGeneric, our generic component that provides values/context, and FieldGeneric, our generic component that consumes values/context from FormGeneric.
*The complete code of ArticleUsingFormGeneric.tsx is shown below
From this link on GitHub, you will be able to access the code used as a basis for developing this article and you will also have access to the book.
Likewise, it is important to exhibit that the valuesInitialization and GetValuesSetValueUseStateManually actions of FormGeneric are functions created after this source as a result of needs that we will indicate below:
If we want to edit a record that has already been created, it means that we already have the necessary values.
In our applications it is very normal to implement CRUD of entities. For example, concerning the person entity, we will have a screen to create a person, read a person, update a person, delete a person, etc.
In the case that we want to edit an already existing person, we will have to start the useState values of our generic component FormGeneric, with the values we pass from ArticleUsingFormGeneric.tsx. Finally, be able to display the values in our generic component FieldGeneric.
As you can see in the image below:
Apart from what we have discussed in the previous point, about the CRUD implementation, many times we need to be able to interact with controls on our screens before proceeding to send information, such as creating a new record, editing a new record, etc.
However, with the implementation that we have created at this point, it is not possible to act directly on the value of the input (control declared in our generic FieldGeneric component) from the ArticleUsingFormGeneric component.
For example, provide a default value for the number input by clicking on “Default value in number field”
In this case, to overcome this limitation, we have GetValuesSetValueUseStateManually, a variable declared in FormGeneric.
This variable allows us to act directly with the values of FormGeneric from any component. No need to use context, which FieldGeneric does.
With GetValuesSetValueUseStateManually we have the values of the useState of FormGeneric from the ArticleUsingFormGeneric control. So we can on the one hand query the current values or introduce/update new ones.
On the other hand, setForceRedendering is nothing more than a useState created for React to render when it suits us.
As we can see in the image below:
Next, we leave you the code of this example. However, this code is a very simple example used as an introduction to the explanation. We recommend starting from Chapter 15, and starting from this code, expanding it step by step with the needs of each project or development.
import React, { useState, useEffect, ChangeEvent, FormEvent } from "react";
import { Page } from "../../components/Page/page";
interface Props {}
export const Article: React.FC<Props> = ({}) => {
const [inputNameValue, setInputNameValue] = useState<string>("");
const [inputSurnameValue, setInputSurnameValue] = useState<string>("");
const requiredField = (value: any): string => {
return value === undefined || value === null || value === ""
? "Required field"
: "";
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
//Field name required
if (requiredField(inputNameValue) === "") {
//Create record
// await createRecord({
// name: inputNameValue,
// surname: inputSurnameValue
// });
} else {
alert("The Name field is required");
}
};
const handleInputNombreChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputNameValue(e.currentTarget.value);
};
const handleInputSurnameChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputSurnameValue(e.currentTarget.value);
};
return (
// <Page>
// </Page>
<>
<h1>
Working with Forms in React and reducing boilerplate code with generic components
</h1>
<form onSubmit={handleSubmit}>
<p>
<input
id="name"
type="text"
placeholder="Name"
value={inputNameValue}
onChange={handleInputNameChange}
/>
</p>
<p>
<input
id="surname"
type="text"
placeholder="Surname"
value={inputSurnameValue}
onChange={handleInputSurnameChange}
/>
</p>
<p>
<button type="submit">Save</button>
</p>
</form>
</>
);
};
import React, { useState, useEffect, ChangeEvent, FormEvent } from "react";
import { Page } from "../../components/Page/page";
import { FieldGeneric } from "./FieldGeneric";
import {
FormGeneric,
GetValuesSetValueUseStateManually,
requiredField,
Values,
} from "./FormGeneric";
interface Props {}
export const ArticleUsingFormGeneric: React.FC<Props> = ({}) => {
const [inputNameValue, setInputNameValue] = useState<string>("");
const [inputSurnameValue, setInputSurnameValue] = useState<string>("");
const [forceRendering, setForceRendering] = useState(false);
const handleSubmit = async (values: Values) => {
let submit = true;
//Create record
// submit = await createRecord({
// name: values.name,
// surname: values.surname
// });
return { success: submit ? true : false };
};
const handleOnClickValueNameDefault = () => {
const CurrentValues = GetValuesSetValueUseStateManually;
GetValuesSetValueUseStateManually["name"] = "Mr. React";
setForceRendering(!forceRendering);
};
return (
// <Page>
// </Page>
<>
<FormGeneric
valuesInitialization={{
name: "Mr. React",
surname: "Pascual",
}}
validationRules={{
name: [{ validator: requiredField }],
}}
onSubmit={handleSubmit}
failureMessage="Error"
successMessage="Ok"
>
<FieldGeneric name="name" label="Name" />
<FieldGeneric name="surname" label="Surname" />
<p>
<button type="button" onClick={handleOnClickValueNameDefault}>
Default value in name field
</button>
</p>
<p>
<button type="submit">Save</button>
</p>
</FormGeneric>
</>
);
};
import { FC, useState, createContext, FormEvent } from "react";
export interface Values {
[key: string]: any;
}
export interface Errors {
[key: string]: string[];
}
export interface Touched {
[key: string]: boolean;
}
interface FormContextProps {
values: Values;
setValue?: (fieldName: string, value: any) => void;
errors: Errors;
validate?: (fieldName: string) => void;
touched: Touched;
setTouched?: (fieldName: string) => void;
}
export let GetValuesSetValueUseStateManually: Values;
export const FormContext = createContext<FormContextProps>({
values: {},
errors: {},
touched: {},
});
type Validator = (value: any, args?: any) => string;
export const requiredField: Validator = (value: any): string =>
value === undefined || value === null || value === ""
? "Cant be empty"
: "";
interface Validation {
validator: Validator;
arg?: any;
}
interface ValidationProp {
[key: string]: Validation | Validation[];
}
export interface SubmitResult {
success: boolean;
errors?: Errors;
}
interface Props {
validationRules?: ValidationProp;
onSubmit: (values: Values) => Promise<SubmitResult>;
successMessage?: string;
failureMessage?: string;
valuesInitialization?: Values;
}
export const FormGenerico: FC<Props> = ({
children,
validationRules,
onSubmit,
successMessage = "Ok",
failureMessage = "Error",
valuesInitialization = {},
}) => {
const [values, setValues] = useState<Values>(valuesInitialization);
const [errors, setErrors] = useState<Errors>({});
const [touched, setTouched] = useState<Touched>({});
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [submitError, setSubmitError] = useState(false);
GetValuesSetValueUseStateManually = values;
const validate = (fieldName: string): string[] => {
if (!validationRules) {
return [];
}
if (!validationRules[fieldName]) {
return [];
}
const rules = Array.isArray(validationRules[fieldName])
? (validationRules[fieldName] as Validation[])
: ([validationRules[fieldName]] as Validation[]);
const fieldErrors: string[] = [];
rules.forEach((rule) => {
const error = rule.validator(values[fieldName], rule.arg);
if (error) {
fieldErrors.push(error);
}
});
const newErrors = { ...errors, [fieldName]: fieldErrors };
setErrors(newErrors);
return fieldErrors;
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (validateForm()) {
setSubmitting(true);
setSubmitError(false);
const result = await onSubmit(values);
setErrors(result.errors || {});
setSubmitError(!result.success);
setSubmitting(false);
setSubmitted(true);
}
};
const validateForm = () => {
const newErrors: Errors = {};
let haveError: boolean = false;
if (validationRules) {
Object.keys(validationRules).forEach((fieldName) => {
newErrors[fieldName] = validate(fieldName);
if (newErrors[fieldName].length > 0) {
haveError = true;
}
});
}
setErrors(newErrors);
return !haveError;
};
return (
<FormContext.Provider
value={{
values,
setValue: (fieldName: string, value: any) => {
setValues({ ...values, [fieldName]: value });
},
errors,
validate,
touched,
setTouched: (fieldName: string) => {
setTouched({ ...touched, [fieldName]: true });
},
}}
>
<form noValidate={true} onSubmit={handleSubmit}>
<fieldset disabled={submitting || (submitted && !submitError)}>
{children}
{submitted && submitError && <p>{failureMessage}</p>}
{submitted && !submitError && <p>{successMessage}</p>}
</fieldset>
</form>
</FormContext.Provider>
);
};
import { FC, useContext, ChangeEvent } from "react";
import { FormContext } from "./FormGeneric";
interface Props {
name: string;
label?: string;
type?: "Text" | "TextArea";
}
export const FieldGeneric: FC<Props> = ({ name, label, type = "Text" }) => {
const { setValue, touched, setTouched, validate } = useContext(FormContext);
const handleChange = (
e: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>
) => {
if (setValue) {
setValue(name, e.currentTarget.value);
}
if (touched[name]) {
if (validate) {
validate(name);
}
}
};
const handleBlur = () => {
if (setTouched) {
setTouched(name);
}
if (validate) {
validate(name);
}
};
return (
<FormContext.Consumer>
{({ values, errors }) => (
<div>
{label && <label>{label}</label>}
{type === "Text" && (
<input
type={type.toLowerCase()}
id={name}
value={values[name] === undefined ? "" : values[name]}
onChange={handleChange}
onBlur={handleBlur}
/>
)}
{type === "TextArea" && (
<textarea
id={name}
value={values[name] === undefined ? "" : values[name]}
onChange={handleChange}
onBlur={handleBlur}
/>
)}
{errors[name] &&
errors[name].length > 0 &&
errors[name].map((error) => <div key={error}>{error}</div>)}
</div>
)}
</FormContext.Consumer>
);
};
We hope that the development of this example will help you to learn how to create forms using components controlled by React and how to build generic components by minimizing and optimizing the code.