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>
);

#api #typescript #context api

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