Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Building a React Form Component with TypeScript: Sharing State via Context API

June 12, 2018
reacttypescript

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.

Contact us form

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:

Using TypeScript with React

Using TypeScript with React
Find out more

Want more content like this?

Subscribe to receive notifications on new blog posts and courses

Required
© Carl Rippon
Privacy Policy