NativeBase X Formik

NativeBase X Formik

Building Forms for websites is painstaking. But since they are the main medium of data collection, every web app needs one. So, whenever a new project starts, a developer needs to plan for a form creation sprint. Tears are sometimes non-optional. 🥲

What if there was a way to refactor the entire process into a single reusable component?

This is what we will discuss in this article. For the logic aspects, we will be using Formik and for the UI, we will be using NativeBase.

Why Formik?

React only offers the view layer for an application and only provides the bare minimum for creating form components. A developer must mix and match components, states, and props together to create a functional form.

Here is an example of how forms are written in pure React or React Native without using any external libraries ⤵️

  const [state, setState] = useState({
    name: "",
    email: "",
    password: "",
  });

Functional, plain, basic.

Let’s change that by adding functionality to the form. We start by updating the state with user inputs. The handleChange function is required to set the state as user types. This is the fundamental unit.

We will get an event from which we pull the value from the event target and store it in the state. Then we wire it up by passing the state into inputs and state handler.

  const handleChange = (name) => (text) => {
    setState({ ...state, [name]: text });
  };

Next, we create handleSubmit function for the form submission. This function will console log the form values and then clear them.

  const handleSubmit = () => {
    console.log(state);
    setState({
      name: "",
      email: "",
      password: "",
    });
  };

Then we render the form elements. We will be using NativeBase components so that we don’t need to worry about accessibility and styling. Also, it becomes easier to style components using the utility props, no need to create any stylesheet classes.

import { Input, VStack, Button } from "native-base";
import React, { useState } from "react";

function NativebaseFormExample() {
  const [state, setState] = useState({
    name: "",
    email: "",
    password: "",
  });

  const handleChange = (name) => (text) => {
    setState({ ...state, [name]: text });
  };

  const handleSubmit = () => {
    console.log(state);
    setState({
      name: "",
      email: "",
      password: "",
    });
  };

  return (
    <VStack>
      <Input
        type="text"
        placeholder="Name"
        onChangeText={handleChange("name")}
        value={state.name}
      />
      <Input
        type="email"
        placeholder="Email"
        onChangeText={handleChange("email")}
        value={state.email}
      />
      <Input
        type="password"
        placeholder="Password"
        onChangeText={handleChange("password")}
        value={state.password}
      />
      <Button onPress={handleSubmit}>
        SUBMIT
      </Button>
    </VStack>
  );
}

Now, we have the complete form.

You'll see that there is a significant amount of code for a form with only three text boxes. Imagine the number of state values you must monitor in a form with 10 or more inputs. And all of this is without form validation or error handling.

Here are the steps of creating a form in React and React Native without external libraries.

  • Initialize the state for the form values
  • Modify the existing state based on user input
  • Creat validation functions
  • Manage submissions
  • Wire it up
  • Test
  • And finally, bashing one’s own face on the wall 😅

When using the React framework it is necessary to write each stage of the form-building process, from state creation through form submission. These are all crucial details that are left up to you. And since React is declarative, it concentrates more on the what rather than the how, and the rest is up to you.

I've developed numerous React forms, and I consistently find the process to be challenging and time-consuming. Sometimes, it also gets boring. Thankfully, I'm not the only one who has this feeling. That is where Formik comes into the picture.

What is Formik?

Formik is a collection of components and hooks for building forms in React and React Native. It helps standardize the input components and the flow of form submission. Its API is straightforward, making it a scalable and effective form helper. It simplifies the three most cumbersome steps in the form creation process:

  1. Getting values in and out of form state
  2. Validation and error messages
  3. Handling form submission

By putting all of the above in one place, Formik keeps things organized. The structured workflow also makes the process of testing, refactoring, and reasoning a breeze.

There are more React form management tools out there, such as Redux Form. However, Redux Form uses reducers. Due to it, the top-level reducer is called numerous times for each keypress on the React form. In smaller apps, making such a request to the reducer with every keystroke is not important. But, as the program expands in size, latency will inevitably occur.

"Form state is local and ephemeral." - Dan Abramov

Integrating Formik with Nativebase

Let’s see how Formik and Nativebase make building forms easier compared to React’s natural way.

First, we need to pass our form’s initialValues and onSubmit to the useFormik() hook. This will return a form state and helper methods in a variable we call formik.

  const formik = useFormik({
    initialValues: { email: "", password: "" },
    onSubmit: (values, { resetForm }) => {
      console.log(values);
      resetForm({ values: "" });
    },
  });

We then pass these helper methods to the respective props in Nativebase Input and Button component. That’s it. That is it!

    <Input
       type="email"
       placeholder="Email"
       onChangeText={formik.handleChange("email")}
       onBlur={formik.handleBlur("email")}
       value={formik.values.email}
  />
    <Input
       type="password"
       placeholder="password"
       onChangeText={formik.handleChange("password")}
       onBlur={formik.handleBlur("password")}
       value={formik.values.password}
  />
    <Button
       onPress={formik.handleSubmit}
       type="submit"
  >
       SUBMIT
  </Button>

We can now have a working form powered by Formik. Instead of managing our form’s values on our own and writing our own custom event handlers for every single input, we can just use useFormik().

Here, we are reusing the exact same change handler function handleChange for each input, which is provided by Formik. There is no need to implement your own handleChange. This will update the formik object’s values object with the new value so that we can retrieve it from the values object in the onSubmit props.

Validation and error messages in Formik

When creating forms, validation is absolutely crucial. A lot of mistakes can occur if forms are not handled correctly. Users must be informed about the fields that are required and the types of values that are permitted in each field. Giving people a crystal-clear explanation of what is incorrect with their input also helps.

Formik includes a method for handling validation and displaying error messages. In the below section, we will learn how to display validation messages and error messages in Formik.

Here, I’ll be using Yup for validating, but it’s 100% optional. You can use whatever you want. Feel free to write your own validators or use a 3rd-party helper library.

import { Input, VStack, Heading, Button, Text } from "native-base";
import React from "react";
import { useFormik } from "formik";
import * as Yup from "yup";

function FormikFormUsingHookExample() {
  const formik = useFormik({
    initialValues: { name: "", email: "", password: "" },
    validationSchema: Yup.object().shape({
      name: Yup.string()
        .min(2, "Too Short!")
        .max(50, "Too Long!")
        .required("Required"),
      email: Yup.string().email("Invalid email").required("Required"),
      password: Yup.string()
        .required("Required")
        .min(8, "Too Short! - should be 8 chars minimum.")
        .matches(/[a-zA-Z]/, "Password can only contain letters."),
    }),
    onSubmit: (values, { resetForm }) => {
      console.log(values);
      resetForm({ values: "" });
    },
  });
  return (
    <VStack>
      <Input
        type="text"
        placeholder="Name"
        onChangeText={formik.handleChange("name")}
        onBlur={formik.handleBlur("name")}
        value={formik.values.name}
      />
      {formik.errors.name && formik.touched.name ? (
        <Text fontSize="xs" color="danger.500" alignSelf="flex-start">
          {formik.errors.name}
        </Text>
      ) : null}
      <Input
        type="email"
        placeholder="Email"
        onChangeText={formik.handleChange("email")}
        onBlur={formik.handleBlur("email")}
        value={formik.values.email}
      />
      {formik.errors.email && formik.touched.email ? (
        <Text fontSize="xs" color="danger.500" alignSelf="flex-start">
          {formik.errors.email}
        </Text>
      ) : null}
      <Input
        type="password"
        placeholder="Password"
        onChangeText={formik.handleChange("password")}
        onBlur={formik.handleBlur("password")}
        value={formik.values.password}
      />
      {formik.errors.password && formik.touched.password ? (
        <Text fontSize="xs" color="danger.500" alignSelf="flex-start">
          {formik.errors.password}
        </Text>
      ) : null}
      <Button
        mt="4"
        size="sm"
        w="full"
        onPress={formik.handleSubmit}
        type="submit"
      >
        SUBMIT
      </Button>
    </VStack>
  );
}

export default FormikFormUsingHookExample;

Formik validates after each keystroke (onChange event), each input’s blur event (weather user has touched the input), as well as prior to submission. The onSubmit function will be executed only if there are no errors.

The Validation Schema then populates the formik.errors with specific keys, which we can access while displaying error messages. The error messages are displayed only if, the input field is touched by the user. Because it doesn’t make any sense to show warnings or errors to a user before they start filling it.

Formik Context API

Formik also provides context APIs to make life easier and code less verbose: <Formik />,

<Form />, <Field />, and <ErrorMessage />. More explicitly, they use React Context implicitly to connect with the parent <Formik /> state/methods.

Formik kind of borrowed the concept of imperative escape hatches from React. React has a bunch of imperative escape hatches. So in Formik, instead of just passing handle change, there are equivalent imperative handlers and helper methods that you can call. These become handy when you are working with third-party components.

The open source community is great and it has a ton of other react components one of those being Nativebase, but the technique I'm about to show you will work on any custom input and is very powerful. It shows that by just focusing on orchestrating the form and its business logic, then giving the right escape hatches, it can become compatible with any sort of input.

It doesn't matter in which environment your input exists and it will work on anywhere react does, like React Native, React-dom, React VR.

Now let’s swap out the useFormik() hook for Formik’s <Formik> render-prop. Since it’s a component, we’ll convert the object passed to useFormik() to JSX, with each key becoming a prop.

And this time we will take one complex example to use Formik with different types of inputs such as Radio and Select component of NativeBase.

import { Input, VStack, Heading, Button, Text, Select, CheckIcon, Radio,
} from "native-base";
import React from "react";
import { Formik } from "formik";
import * as Yup from "yup";

// Validation Object
const SignUpSchema = Yup.object().shape({
  name: Yup.string()
    .min(2, "Too Short!")
    .max(50, "Too Long!")
    .required("Required"),
  email: Yup.string().email("Invalid email").required("Required"),
  password: Yup.string()
    .required("Required")
    .min(8, "Too Short! - should be 8 chars minimum.")
    .matches(/[a-zA-Z]/, "Password can only contain letters."),
});

// With Validation
function FormikValidFormExample() {
  return (
    <Formik
      initialValues={{
        name: "",
        email: "",
        password: "",
        service: "ux",
        gender: "m",
      }}
      validationSchema={SignUpSchema}
      onSubmit={(values, { resetForm }) => {
        console.log(values);
        resetForm({ values: "" });
      }}
    >
      {({
        handleChange,
        handleBlur,
        handleSubmit,
        values,
        errors,
        touched,
      }) => (
        <VStack>
          <Input
            type="text"
            placeholder="Name"
            onChangeText={handleChange("name")}
            onBlur={handleBlur("name")}
            value={values.name}
          />
          {errors.name && touched.name ? (
            <Text fontSize="xs" color="danger.500">
              {errors.name}
            </Text>
          ) : null}
          <Input
            type="email"
            placeholder="Email"
            onChangeText={handleChange("email")}
            onBlur={handleBlur("email")}
            value={values.email}
          />
          {errors.email && touched.email ? (
            <Text fontSize="xs" color="danger.500">
              {errors.email}
            </Text>
          ) : null}
          <Input
            type="password"
            placeholder="Password"
            onChangeText={handleChange("password")}
            onBlur={handleBlur("password")}
            value={values.password}
          />
          {errors.password && touched.password ? (
            <Text fontSize="xs" color="danger.500">
              {errors.password}
            </Text>
          ) : null}
          <Select
            selectedValue={values.service}
            minWidth="200"
            placeholder="Choose Service"
            _selectedItem={{
              bg: "teal.600",
              endIcon: <CheckIcon size="5" />,
            }}
            mt={1}
            onValueChange={handleChange("service")}
          >
            <Select.Item label="UX Research" value="ux" />
            <Select.Item label="Web Development" value="web" />
          </Select>
          <Radio.Group
            name="gender"
            value={values.gender}
            onChange={handleChange("gender")}
          >
            <Radio value="m" my={1}>
              Male
            </Radio>
            <Radio value="f" my={1}>
              Female
            </Radio>
          </Radio.Group>
          <Button
            onPress={handleSubmit}
            type="submit"
          >
            SUBMIT
          </Button>
        </VStack>
      )}
    </Formik>
  );
}

export default FormikValidFormExample;

This shows how flexible this kind of render props and APIs like can be.

Wrapping Up

Congratulations! Now we know how to integrate Formik with NativeBase, and create a form that:

  • Has complex logic for validation and rich error messages
  • Shows error messages and warnings only for visited fields
  • Has pre-styled custom input components, which you can use on any other forms in your app

Check out the Github Repo of final result here: Final Result

May your forms be forever in your favor!