Use Ant Design Form with React Context API

Use Ant Design Form with React Context API

Ant Design is a popluar components framework for React. One of the components is <Form>. It provides built-in functions for collect, validate and submit user input.

<Form> in Ant Design is using a decorator pattern for the fields. This is not a problem until you want to separate the form and fields.

Here is an simple example.

import React from "react";
import { Button, Form, Input } from "antd";
import { FormComponentProps } from "antd/lib/form";

type LoginFormProps = FormComponentProps;
function LoginFormImpl(props: LoginFormProps): JSX.Element {
  const { form } = props;
  const { getFieldDecorator } = form;
  return (
    <Form>
      <Form.Item>
        {getFieldDecorator("userName", {
          rules: [{ required: true, message: "Please input your username!" }]
        })(<Input placeholder="Username" />)}
      </Form.Item>
      <Form.Item>
        {getFieldDecorator("password", {
          rules: [{ required: true, message: "Please input your Password!" }]
        })(<Input type="password" placeholder="Password" />)}
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          Login
        </Button>
      </Form.Item>
    </Form>
  );
}

export const LoginForm = Form.create()(LoginFormImpl);

It is just a simple login form. But when you have a more complex form or when you want to reuse a field, then you have to pass the form around to access the functionalities.

function LoginFormImpl(props: LoginFormProps): JSX.Element {
  const { form } = props;
  return (
    <Form>
      <Form.Item>
        <UserNameInput form={form} />
      </Form.Item>
      <Form.Item>
        <PasswordInput form={form} />
      </Form.Item>
      <Form.Item>
        <LoginButton form={form} />
      </Form.Item>
    </Form>
  );
}

This become much cleaner. However, if it is not a single component but another large component, then, you will have to pass form through every components in the middle which is not ideal.

React Context API is the exact solution for what we needed.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

import { createContext } from "react";
import { WrappedFormUtils } from "antd/lib/form/Form";

export const FormContext = createContext<WrappedFormUtils<any> | null>(null);

We can use <FormContext.Provider> and <FormContext.Consumer> but we have to do null checking and add form decorator every time. We can create a wrapper for those.

import React, { ReactNode } from "react";
import { Form } from "antd";
import { FormComponentProps } from "antd/lib/form";

import { FormContext } from "./context";

type FormContextProviderProps = { children?: ReactNode } & FormComponentProps;
function FormContextProviderImpl(props: FormContextProviderProps): JSX.Element {
  const { form, children } = props;
  return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
}

export const FormContextProvider = Form.create()(FormContextProviderImpl);
import React, { ReactNode } from "react";
import { WrappedFormUtils } from "antd/lib/form/Form";

import { FormContext } from "./context";

type FormContextConsumerProps = {
  children: (value: WrappedFormUtils) => ReactNode;
};
export function FormContextConsumer(
  props: FormContextConsumerProps
): JSX.Element {
  const { children } = props;

  return (
    <FormContext.Consumer>
      {form => {
        if (!form) {
          throw new Error("Missing FormContextProvider in its parent.");
        }
        return children(form);
      }}
    </FormContext.Consumer>
  );
}
import { useContext } from "react";
import { WrappedFormUtils } from "antd/lib/form/Form";

import { FormContext } from "./context";

export function useFormContext(): WrappedFormUtils {
  const form = useContext(FormContext);
  if (!form) {
    throw new Error("Missing FormContextProvider in its parent.");
  }
  return form;
}

Those three pieces of code above are provider, consumer and React hook respectively. Then, we can refactor the form code.

export function LoginForm(): JSX.Element {
  return (
    <FormContextProvider>
      <Form>
        <Form.Item>
          <UserNameInput />
        </Form.Item>
        <Form.Item>
          <PasswordInput />
        </Form.Item>
        <Form.Item>
          <LoginButton />
        </Form.Item>
      </Form>
    <FormContextProvider/>
  );
}
export function UserNameInput(): ReactNode {
  const { getFieldDecorator } = useFormContext();
  return getFieldDecorator("userName", {
    rules: [{ required: true, message: "Please input your username!" }]
  })(<Input placeholder="Username" />);
}

Now, we no longer need to pass through form to each component. With Context API, it automatically use nearest parent provider. This means use do not need to update the fields to reuse them.