Skip to main content

How to write a form that commits a mutation?

In this guide you'll learn how to use helpers provided by ShipFast to work with react-hook-form to manage state of a form, commit a mutation when user submits it, and render errors returned from back-end.

info

If you're looking for back-end implementation related to this guide take a look at Adding new mutation guide.

Create a new React component

Let's begin by creating a new React component named ProductForm. You can refer to the Create React component guide to create an empty one.

Define a type describing form fields data

To fully leverage the benefits of typing, you will need to define a type that describes the data collected by the form. For this guide, you can use the following type:

src/routes/productForm/productForm.types.ts
export type ProductFormFields = {
name: string;
};

To prevent potential circular dependency issues, we suggest placing this type in a separate file named productForm.types.

tip

You can use yup to define a validation schema and infer the type automatically. react-hook-form has a built-in support for such schema.

Define a custom hook to manage form state

It's a good practice to encapsulate the logic of a form in a custom hook. As the logic tends to grow in complexity, this approach has the potential to separate the presentation and business logic, making the code more manageable and easier to maintain.

packages/webapp/src/routes/productForm/productForm.hooks.ts
export const useCreateProductForm = () => {
const form = useApiForm<ProductFields>();

const handleSubmit = form.handleSubmit(async (data: ProductFields) => {
// You'll commit mutation here
});

return { form, handleSubmit };
};

useApiForm – custom wrapper around useForm hook from react-hook-form that helps you to work with

ShipFast GraphQL API.

Implement empty form component

Since you already have a custom hook, the next step is to create an empty form that includes only a submit button. This will allow you to verify whether your configuration is correct.

Take handleSubmit returned from useCreateProductForm and pass it to form.onSubmit prop.

packages/webapp/src/routes/productForm/productForm.component.tsx
import { Button } from '@shipfast/webapp-core/components/buttons';
import { FormattedMessage } from 'react-intl';

import { useCreateProductForm } from './productForm.hooks'

export const ProductForm = () => {
const { handleSubmit } = useCreateProductForm();

return (
<form onSubmit={handleSubmit}>
<Button type="submit">
<FormattedMessage defaultMessage="Submit" id="Product form / Submit button" />
</Button>
</form>
);
};
  • Button – reusable button component from @shipfast/webapp-core. You can customize it as much as you want.
  • FormattedMessage – This component uses the formatMessage API and has props that correspond to a Message Descriptor.

Add first form control

Next you can add an actual form control to collect the name value our mutation expects to receive. You should use react-intl to format any labels, placeholders, and error messages.

packages/webapp/src/routes/productForm/productForm.component.tsx
import { Input } from '@shipfast/webapp-core/components/forms';
import { Button } from '@shipfast/webapp-core/components/buttons';
import { FormattedMessage, useIntl } from 'react-intl';

import { useCreateProductForm } from './productForm.hooks'

export const ProductForm = () => {
const intl = useIntl();
const {
form: {
register,
formState: { errors, isSubmitting },
},
handleSubmit,
} = useCreateProductForm();

return (
<form onSubmit={handleSubmit}>
<Input
{...register('name', {
required: {
value: true,
message: intl.formatMessage({
defaultMessage: 'Name is required',
id: 'Product form / Name required',
}),
},
})}
label={intl.formatMessage({
defaultMessage: 'Name:',
id: 'Product Form / Name label',
})}
placeholder={intl.formatMessage({
defaultMessage: 'Name',
id: 'Product form / Name placeholder',
})}
error={errors.name?.message}
/>

<Button type="submit" disabled={isSubmitting}>
<FormattedMessage defaultMessage="Submit" id="Product form / Submit button" />
</Button>
</form>
);
};
  • form.register – Method that allows you to register an input or select element and apply validation rules
  • Input – Controlled input component compatible with react-hook-form.

Define GraphQL mutation

Proceed to create a new file named productForm.graphql.ts. In this file, implement a mutation using the autogenerated gql helper function that resides in @shipfast/webapp-api-client package. Adding this mutation will automatically generate all necessary TypeScript types inside @shipfast/webapp-api-client package.

packages/webapp/src/routes/productForm/productForm.graphql.ts
import { gql } from '@shipfast/webapp-api-client/graphql';

export const createProductMutation = gql(/* GraphQL */ `
mutation createProductMutation($input: CreateProductMutationInput!) {
createProduct(input: $input) {
productEdge {
node {
id
name
}
}
}
}
`);

Commit mutation in form hook

packages/webapp/src/routes/productForm/productForm.hooks.ts
import { useMutation } from '@apollo/client';

import { createProductMutation } from './productForm.graphql'

export const useCreateProductForm = () => {
const form = useApiForm<ProductFields>();

const [commitMutation] = useMutation(createProductMutation, {
onError: (error) => {
form.setApolloGraphQLResponseErrors(error.graphQLErrors);
}
});

const handleSubmit = form.handleSubmit(async (data: ProductFields) => {
await commitMutation({ variables: { input: data } });
});

return { form, handleSubmit };
};
  • useMutation – Apollo client hook that helps modify back-end data with mutations.
  • Using async handler makes sure the form.formState.isSubmitting prop is true when the request is in flight.

Key points

  • Use useApiForm hook to control form state
  • Commit mutations inside custom hooks instead of directly in components to decouple logic from presentation layer
  • Use react-intl to format input labels, placeholders, and error messages