DEV Community

Cover image for Building Forms with Formik and React - Part 1
Farley Knight
Farley Knight

Posted on

Building Forms with Formik and React - Part 1

About This Post

Hello there!

This post will focus on creating some forms using React! Form building is a frequently visited topic in web development. Many aspects of web development have something to do with forms. Form builders, form serializers, form validation, etc, etc.

Looking at the web component of React, we have two versions: controlled and uncontrolled <input>s. One of the main advantages of using React is the fine-grained control of state and how it is handled in the user interface.

When an <input> is uncontrolled, it behaves exactly as it does in the ordinary DOM: the <input> itself handles its own state. The other option are controlled <input>s, which are React components that wrap DOM fields, which store the field state inside the component and renders when it is changed. Because controlled components are such a common use-case, a form-handling library called Formik has emerged in the React ecosystem.

This post will cover creating a login form in React first. In future posts, we'll be using Formik to build the actual form, and then we'll use Formik to build the form with Yup to specify a validation schema, instead of hand-written validation methods.

Creating a Sample React.JS Application

Let's first create a React.JS application with create-react-app and run it.

$ npx create-react-app building-forms
npx: installed 99 in 16.195s

Creating a new React app in .../building-forms.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...
...
$ cd building-forms
$ npm start

After waiting a minute, you should see:

Compiled successfully!

You can now view building-forms in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.1.9:3000

Note that the development build is not optimized.
To create a production build, use yarn build.

And then your browser will open localhost:3000 with the React logo spinning:

Alt Text

In the next section, we'll replace the logo and other text with a login form.

Building a Login Form

Before we get into Formik and why it is preferable for React form development, let's take a look at src/App.js.

// App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

We're going to hollow out this App component and replace it with a LoginForm, which isn't fleshed out yet. Replace your src/App.js with the following content.

// App.js with LoginForm stub
import React from 'react';
import './App.css';

const LoginForm = () => "Login form goes here"

const App = () => {
  return (
    <div className="App">
      <header className="App-header">
        <LoginForm/>
      </header>
    </div>
  );
}

export default App;

You shouldn't be surprised to see a very boring message at the center of your screen:

Alt Text

Since the LoginForm component is just a stub, we defined it in the App.js file. In the next step, we're going to flesh it out and give it it's own file.

Fleshing Out the Login Form

Create the file src/LoginForm.js and add the following:

// LoginForm.js
import React from 'react';

const LoginForm = () => {
  return (
    <form>
      <div>
        <label>Email Address</label>
        <input type="email" name="email" placeholder="Enter email"/>
      </div>
      <div>
        <label>Password</label>
        <input type="password" name="password" placeholder="Password"/>
      </div>
      <button type="submit">Log in</button>
    </form>
  );
}

export default LoginForm;

Next update your src/App.js to use import for the LoginForm.

// App.js with LoginForm import
import React from 'react';
import LoginForm from './LoginForm';
import './App.css';

const App = () => {
  return (
    <div className="App">
      <header className="App-header">
        <LoginForm/>
      </header>
    </div>
  );
}

export default App;

As you can see, the result is pretty much garbage as far as design / UX is concerned:

Alt Text

Let's fix that by adding Twitter Bootstrap to our React.JS application.

Adding Bootstrap to our React Project

We can install Bootstrap to our existing React.JS application with the npm install --save bootstrap command. This command should install the package, but also add this project to the package.json, which keeps track of the dependencies for our project.

$ npm install --save bootstrap
npm WARN deprecated left-pad@1.3.0: use String.prototype.padStart()
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
...

The installation process can take a while. When it's done, make sure bootstrap is in your package.json.

/* package.json */
"dependencies": {
  "@testing-library/jest-dom": "^4.2.4",
  "@testing-library/react": "^9.3.2",
  "@testing-library/user-event": "^7.1.2",
  "bootstrap": "^4.4.1", /* Make sure you see this line */
  "react": "^16.12.0",
  "react-dom": "^16.12.0",
  "react-scripts": "3.4.0"
},

In the src/index.js you can add this line:

// index.js
import './index.css';
// Add this line below the `index.css` include
import 'bootstrap/dist/css/bootstrap.css';

Now let's update our LoginForm component to use Bootstrap styles.

// LoginForm.js with Bootstrap classes & styles
import React from 'react';

const LoginForm = () => {
  return (
    <div className="container">
      <div className="row justify-content-center">
        <div className="col-lg-6">
          <div className="col-lg-12">
            <form>
              <div className="form-group">
                <label>Email Address</label>
                <input
                  type="email"
                  name="email"
                  placeholder="Enter email"
                  className="form-control"          
                />
              </div>
              <div className="form-group">
                <label>Password</label>
                <input
                  type="password"
                  name="password"
                  placeholder="Password"
                  className="form-control"
                />
              </div>
              <button type="submit" className="btn btn-primary btn-block">
                Log in
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
}

export default LoginForm;

Alt Text

Form Validation in React.JS

We've got a good looking login form, but it has some problems.

For one, the form will send an HTTP GET request to the root URL / instead of a POST to some authentication URL, like /sessions. The backend has not been prepared to receive the payload, so we'll only be redirected to the same page. However, we're not going to worry about this detail at the moment. A future blog post will cover adding a backend to a React.JS application.

The primary goal is to do form validation. Before introducing Formik, we're going to do this validation in React.JS only.

Form state in React.JS

Before we can even get into validation, we need to rewrite our code. Since React forms maintain state, we need to rewrite this component. Instead of using a functional component, we switch to a class that allows us to use this.state and this.setState to set and render values.

// LoginForm.js as a class component with form state
import React from 'react';

class LoginForm extends React.Component {
  // In order to capture the state of the form, we need to initialize the `state`
  // property in the constructor.
  constructor(props) {
    super(props);
    this.state = {
      values: {
        email: "",
        password: ""
      }
    };
  }  

  // We've moved the HTML into the `render` method, and we've added a change
  // handler that will update the component state as the fields are changed.
  // In addition to the change handler, we've also connected the `value` of the
  // <input>s to the component's state.
  render() {
    return (
      <div className="container">
        <div className="row justify-content-center">
          <div className="col-lg-6">
            <div className="col-lg-12">
              <form>
                <div className="form-group">
                  <label>Email Address</label>
                  <input
                    type="email"
                    name="email"
                    placeholder="Enter email"
                    className="form-control"
                    /* Add onChange and value props */
                    onChange={this.onChange.bind(this)}
                    value={this.state.values.email}
                  />
                </div>
                <div className="form-group">
                  <label>Password</label>
                  <input
                    type="password"
                    name="password"
                    placeholder="Password"
                    className="form-control"
                    /* Add onChange and value props */                    
                    onChange={this.onChange.bind(this)}
                    value={this.state.values.password}
                  />
                </div>
                <button type="submit" className="btn btn-primary btn-block">
                  Log in
                </button>
              </form>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // In our change handler, we update the form's `values` based on the input
  // `name`.
  onChange(event) {
    let { values } = this.state;
    values[event.target.name] = event.target.value;
    this.setState({ values });
  }
}

export default LoginForm;

Test out your form again to make sure it still works. Since this update only changes where we store and how we render the form values, we should not expect a difference in behavior.

Alt Text

Writing Validations in React.JS

Now that we've updated our form component to keep track of the field's values, we can add a validation step. We're not going to repeat the entire component here, but only call out the changes we've made.

First, we need to add a place in our state to keep track of what fields are valid or invalid.

class LoginForm extends React.Component {
  ...
  constructor(props) {
    super(props);
    this.state = {
      values: {
        email: "",
        password: ""
      },
      /* Add these two new section to your `state`. */
      /* All fields are invalid by default. */
      valid: {
        email: false,
        password: false
      },
      /* All fields have an empty error message by default. */      
      errors: {
        email: "",
        password: ""
      }
    };
  }
  ...
}

Next, we'll add a submit handler, giving us a place to trigger the validation. We'll also add a new method called validate that will:

  • Check that the email field matches the email regex.
  • Check that the password is six characters or more.
class LoginForm extends React.Component {
  ...
  onSubmit(event) {
    event.preventDefault();
    let { values } = this.state;
    for (let key in values) {
      this.validate(key, values[key]);
    }
  }
  validate(name, value) {
    let { errors, valid } = this.state;

    if (name === "email") {
      /* This complicated regex checks that a string follows the standard
         format for email addresses, e.g. user@example.com
      */
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
      if (!emailRegex.test(value)) {
        errors.email = 'Email should be a valid email address';
      }
    }

    if (name === "password") {
      if (value.length < 6) {
        errors.password = 'Password should be at least 6 characters';
      }
    }

    this.setState({ valid, errors });
  }
  ...
}

Let's add the submit handler to our form, so it gets called when the user attempts to do a submit.

<div className="col-lg-6">
  <div className="col-lg-12">
    <form onSubmit={this.onSubmit.bind(this)}> /* Add the submit handler here */
      <div className="form-group">
        <label>Email Address</label>

Now that we keep track of valid fields and field errors, we can provide a better user experience by validating the form immediately, instead of waiting for a response from the server.

At the top of the render method, let's destruct the state:

render() {
  /* Add this line above the return */
  let { values, valid, errors } = this.state;
  return (
    <div className="container">
      <div className="row justify-content-center">
        ...
      </div>
    </div>
  );
}

Next, we'll look for the <input> sections of the component. In particular, the email field. We'll change the markup to:

  • Add is-invalid to the CSS classes if the email is not valid.
  • Use values.email, which shorter than this.state.values.email.
  • Add a new <div> for errors, with class name invalid-feedback.
<div className="form-group">
  <label>Email address</label>
  <input
    type="email"
    name="email"
    className={`form-control ${valid.email ? "" : "is-invalid"}`}    
    placeholder="Enter email"
    onChange={this.onChange.bind(this)}
    value={values.email}
  />
  <div className="invalid-feedback">{errors.email}</div>
</div>

A similar set of changes need to happen for the password <input>.

<div className="form-group">
  <label>Password</label>
  <input
    type="password"
    name="password"
    className={`form-control ${valid.password ? "" : "is-invalid"}`}
    placeholder="Password"
    onChange={this.onChange.bind(this)}
    value={values.password}
  />
  <div className="invalid-feedback">{errors.password}</div>
</div>

After making all of those changes, your LoginForm class should look like this:

class LoginForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      values: {
        email: "",
        password: ""
      },
      valid: {
        email: false,
        password: false
      },
      errors: {
        email: "",
        password: ""
      }
    };
  }

  onSubmit(event) {
    event.preventDefault();
    let { values } = this.state;
    for (let key in values) {
      this.validate(key, values[key]);
    }
  }

  validate(name, value) {
    let { errors, valid } = this.state;

    if (name === "email") {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
      if (!emailRegex.test(value)) {
        errors.email = 'Email should be a valid email address';
      }
    }

    if (name === "password") {
      if (value.length < 6) {
        errors.password = 'Password should be at least 6 characters';
      }
    }

    this.setState({ valid, errors });
  }

  onChange(event) {
    let { values } = this.state;
    values[event.target.name] = event.target.value;
    this.setState({ values });
  }

  render() {
    console.log(this.state);
    let { values, valid, errors } = this.state;
    return (
      <div className="container">
        <div className="row justify-content-center">
          <div className="col-lg-6">
            <div className="col-lg-12">
              <form onSubmit={this.onSubmit.bind(this)} novalidate>
                <div className="form-group">
                  <label>Email address</label>
                  <input
                    type="text"
                    name="email"
                    className={`form-control ${valid.email ? "" : "is-invalid"}`}
                    placeholder="Enter email"
                    onChange={this.onChange.bind(this)}
                    value={values.email}
                  />
                  <div className="invalid-feedback">{errors.email}</div>
                </div>
                <div className="form-group">
                  <label>Password</label>
                  <input
                    type="password"
                    name="password"
                    className={`form-control ${valid.password ? "" : "is-invalid"}`}
                    placeholder="Password"
                    onChange={this.onChange.bind(this)}
                    value={values.password}
                  />
                  <div className="invalid-feedback">{errors.password}</div>
                </div>
                <button type="submit" className="btn btn-primary btn-block">
                  Log in
                </button>
              </form>
            </div>
          </div>
        </div>
      </div>
    );
  }
};

Let's test what this looks like in the browser:

Alt Text

Next post: Refactor using Formik

In the next post we will work on refactoring our code using a popular React library called Formik, which can handle much of the boilerplate with building React forms.

Thanks for reading, if you have any comments/questions feel free to share them down below! They are always appreciated. :)

Top comments (0)