Building a React Form Component with TypeScript: Sharing State via Context API
This is the third post in a series of blog posts where we are building our own super simple form component in React and TypeScript. In the last post we created our basic Form
and Field
components. In this post we’ll use the context api to share state and functions between Form
and Field
. This will enable us to start to manage the field values properly.
Sharing state and providing delegates using context api
At the moment our form isn’t tracking the field values. We obviously need to do this for when we submit the form to the api. So, how can we push the field values from Field
to Form
? Well the react context api allows components to share state, so, let’s give this a try.
First let’s create the context at the top of Form.tsx. The TypeScript types require a default value for React.createContext
which we’ll set to undefined
:
export interface IFormContext
extends IFormState {
/* Function that allows values in the values state to be set */
setValues: (values: IValues) => void;
}
/*
* The context which allows state and functions to be shared with Field.
* Note that we need to pass createContext a default value which is why undefined is unioned in the type
*/
export const FormContext = React.createContext<IFormContext|undefined>(undefined);
We also need to add the setValues
method in Form.tsx
:
/**
* Stores new field values in state
* @param {IValues} values - The new field values
*/
private setValues = (values: IValues) => {
this.setState({ values: { ...this.state.values, ...values } });
};
Let’s setup the context provider in render()
in Form.tsx
:
public render() {
const { submitSuccess, errors } = this.state;
const context: IFormContext = { ...this.state, setValues: this.setValues };
return (
<FormContext.Provider value={context}> <form onSubmit={this.handleSubmit} noValidate={true}>
...
</form>
</FormContext.Provider> );
}
Whilst we are in Form.tsx
, we’ll output the form values to the console when the form is submitted. This will allow us to check that the values are being properly managed:
private handleSubmit = async (
e: React.FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault();
console.log(this.state.values);
if (this.validateForm()) {
...
}
};
Let’s move on to consuming the context in Field.tsx
…
We first need to import FormContext
:
import {
IErrors,
IFormContext,
FormContext,
} from "./Form";
We can consume this in the return
statement to give us the IFormContext from the form. We can then call setValues
from the editor change events to push new values to the Form
component.
return (
<FormContext.Consumer> {(context: IFormContext) => ( <div className="form-group">
{label && <label htmlFor={id}>{label}</label>}
{editor!.toLowerCase() === "textbox" && (
<input
id={id}
type="text"
value={value}
onChange={
(e: React.FormEvent<HTMLInputElement>) =>
context.setValues({ [id]: e.currentTarget.value }) }
onBlur={
(e: React.FormEvent<HTMLInputElement>) =>
console.log(e) /* TODO: validate field value */
}
className="form-control"
/>
)}
{editor!.toLowerCase() === "multilinetextbox" && (
<textarea
id={id}
value={value}
onChange={
(e: React.FormEvent<HTMLTextAreaElement>) =>
context.setValues({ [id]: e.currentTarget.value }) }
onBlur={
(e: React.FormEvent<HTMLTextAreaElement>) =>
console.log(e) /* TODO: validate field value */
}
className="form-control"
/>
)}
{editor!.toLowerCase() === "dropdown" && (
<select
id={id}
name={id}
value={value}
onChange={
(e: React.FormEvent<HTMLSelectElement>) =>
context.setValues({ [id]: e.currentTarget.value }) }
onBlur={
(e: React.FormEvent<HTMLSelectElement>) =>
console.log(e) /* TODO: validate field value */
}
className="form-control"
>
{options &&
options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
)}
{/* TODO - display validation error */}
</div>
)}
</FormContext.Consumer>
);
We should now be able to submit a form and see the values output to the console when we press the Submit button.
Wrapping up
Our components are progressing nicely. Leveraging the context api means that the consumers of Form
and Field
won’t need to write any change handlers. There is more boiler plate code we want to do without though … like validation. In the next post we’ll do just that.
`
If you to learn more about using TypeScript with React, you may find my course useful: