What I Learned at Work this Week: Formik in Production

Mike Diaz
6 min readApr 24, 2022
Photo by Chris F: https://www.pexels.com/photo/smooth-round-colorful-shapes-with-wavy-edges-6664375/

After I wrote my blog last week, I got a great response from one of my company’s front end engineers. She encouraged me to check out her team’s Formik components since they put the concepts I’ve studied into practice. I’m always happy to get guidance from another engineer, so I figured I’d give it a shot! Let’s examine Form.tsx as it’s written in my workplace (with some changes for privacy).

Breaking down the file

There’s a risk to writing about Formik: it’s so user-friendly that there might not be anything about a component that needs explaining. Fortunately for me, this component incorporates TypeScript, which always complicates things (and, to TypeScript’s credit, then eventually simplifies them). Before we jump into that, we have 21 lines of imports and our first constant definition, which does not use TS:

const StyledFormikForm = styled(FormikForm, {});

I skipped all the imports because it makes more sense to figure out what they do as we find them in the code. Here, we’re using a function called styled, which comes from a file called stitches.config.ts. Loyal readers might remember learning that Stitches is a styling library that my company started using to standardize our front end displays. If you regularly use Stitches, you probably know that the styled function accepts two parameters: an element type and an object which sets styles. It returns a styled component.

The code we’re seeing is surprising because the second argument here is an empty object. Why would we use styled if we’re not going to pass any styling? I had to go back to my old blog and look at some examples from the codebase to figure it out: we want to allow for easy styling of this component when it’s implemented, but we don’t want to add anything by default.

Remember that we’re working on Form.tsx, about as basic as we can get for a product that uses a bunch of different forms. The architects of the component in my company didn’t want to be prescriptive about any style for these forms, so the object is empty. But when we implement the component, we can easily add styling with Stitches syntax:

<Form.FormField name="password" css={{ mb: '$space8' }}>

We’ll get to the logic behind Form.FormField towards the end of this post, but for now just note css={{ mb: '$space8' }}. This syntax only works because we’re passing it to a styled component.

Types

Next, we establish two custom types:

type FormikFormProps = React.ComponentProps<typeof StyledFormikForm>;type FormProps<V extends FormikValues> = Omit<FormikFormProps, 'onSubmit'> & {
initialValues: FormikConfig<V>['initialValues'];
validate?: FormikConfig<V>['validate'];
validationSchema?: AnyObjectSchema;
onSubmit: FormikConfig<V>['onSubmit'];
};

We’ll later use these types to assure that our variables include certain properties. The first, FormikFormProps, uses ComponentProps to specify the type of props that are passed to a component. In this case, we’re saying that FormikFormProps shares properties with the data passed in when a StyledFormikForm is implemented. In the imported Formik code, we can see a very long definition of Form, which we alias as FormikForm, which we remember is the origin of StyledFormikForm:

export declare const Form: React.ForwardRefExoticComponent<Pick<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, "acceptCharset" | "action" | "autoComplete" | "encType" | ...
// this goes on for about 100 more options

In other words, our props can include acceptCharset, action, or any of the other options listed. I found this very difficult to understand, but this Stack Overflow question helped me a bit.

We establish FormikFormProps purely to be used in FormProps, which starts with a generic notation while extending FormikValues:

type FormProps<V extends FormikValues>

V is the generic here, meaning it’s a variable that will be defined when we execute code that references FormProps. I expect that V stands for value or values — the required properties for rendering whatever specific form ends up extending the type we’re defining. Whatever it ends up being, it’ll extend FormikValues, a simple interface that defines the result as an object where keys are strings. This is confusing, but the point of a generic is that we can reference it again later, so hang on if you’re feeling lost.

As we define the type, we bring back FormikFormProps:

Omit<FormikFormProps, 'onSubmit'>

Omit is a TypeScript utility type that accepts two parameters: a type and the keys we want to omit from that type. We’re saying that we want our type to allow for all the props that could be passed to a FormikForm except for onSubmit. As we can see in the next steps, that’s because we’re making onSubmit a required property:

& {
initialValues: FormikConfig<V>['initialValues'];
validate?: FormikConfig<V>['validate'];
validationSchema?: AnyObjectSchema;
onSubmit: FormikConfig<V>['onSubmit'];
};

In addition to the long list of properties made optional by default, we’re adding initialValues, validate, validationSchema, and onSubmit. The first and last are required while the validation props are optional. All except validationSchema are of type FormikConfig<V>. Here’s why the generic is useful: each time FormProps is utilized, it may have a unique structure because we’re using it to create different types of forms. In the type definition, each unique structure is represented by the generic, so if there’s a form where initialValues looks like this:

{
name : '',
email: '',
}

That props object will be valid because we declare it to be the standard. But we can also use FormProps to create a different form where initialValues look like this:

{
firstName: '',
lastName: '',
}

Ultimately, what this is saying is that the FormProps type is an object that can have any of the many properties offered by FormikForm but will most likely look something like this (question marks indicate that the values can be any type so we they can look like anything):

{
initialValues: ???
validate: ???
validationSchema: AnyObjectSchema;
onSubmit: ???
}

React

We survived most of the TypeScript! Take a deep breath and pat yourself on the back. Now it’s time to see how these types play out in React:

function useForm<V extends FormikValues>() {
const formContext = useFormikContext<V>();
return formContext;
}

We’re again using the generic V allow for variance depending on implementation. We define a constant formContext, which is the result of invoking the function useFormikContext. According to that documentation (emphasis mine):

useFormikContext() is a custom React hook that will return all Formik state and helpers via React Context.

In our case, it returns state based on the generic that we pass: the values that will be used by a specific form. We won’t end up using this defined function within Form.tsx, but will export it so that it can be used to return state for the Forms we implement. Speaking of which, we’ve finally reached the Form component!

function Form<V>({
initialValues,
validate,
validationSchema,
onSubmit,
children,
...rest
}: FormProps<V>) {
return (
<Formik
initialValues={initialValues}
validate={validate}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
<FormValidationProvider value={validationSchema || null}>
<StyledFormikForm {...rest}>{children}</StyledFormikForm>
</FormValidationProvider>
</Formik>
);
}

Form is a functional component which again uses the V generic. We’re now familiar with this pattern: depending on what type of form we’re rendering, we’ll have a different values structure which will determine the props we can pass to our component. Form accepts an object as its parameter where initialValues, validate, validationSchema, and onSubmit are passed into a Formik component. We recognize these as the props we defined in FormProps, as well as values that make Formik and Yup work.

The next parameter is children, which comprises all the child components that make up our form (fields, labels, checkboxes, etc). Finally, ...rest is spread because it can be any number of style properties which we pass in when rendering StyledFormikForm.

Exports

We don’t export the Form component directly. Instead, we run it through a transformer that turns it into a sort of enumerable for different, more specific form components:

const Namespace = compositeComponent(Form, {
FormField,
Label,
Checkbox,
DatePicker,
ErrorText,
HelperText,
MultiSelect,
RadioGroup,
SearchableSelect,
Select,
Switch,
TextArea,
TextInput,
ResetButton,
SubmitButton,
});

compositeComponent returns an object that allows us to reference each of these imported form components through dot notation, like we saw in my Stitches example:

<Form.FormField name="password" css={{ mb: '$space8' }}>

This organizational strategy allows us to import all possible form components in one file and export them as a single object that can be more conveniently imported elsewhere. Here’s the export that closes the file:

export { Namespace as Form, useForm };

TypeScript

This ended up being a much bigger lesson in TypeScript than Formik, which tends to happen when I’m studying open source libraries. To understand a production codebase, we have to develop our JavaScript, TypeScript, React, and ancillary understanding together or we’ll quickly hit a wall. Every day is a success if we’ve learned something new, or even practiced previously gained knowledge.

Sources

--

--