DEV Community

Cover image for ๐Ÿ”ฅ Stop Writing Boilerplate! How Formik + React Hooks Can Save You 500 Hours This Year ๐Ÿ”ฅ
Yevhen Kozachenko ๐Ÿ‡บ๐Ÿ‡ฆ
Yevhen Kozachenko ๐Ÿ‡บ๐Ÿ‡ฆ

Posted on • Originally published at ekwoster.dev

๐Ÿ”ฅ Stop Writing Boilerplate! How Formik + React Hooks Can Save You 500 Hours This Year ๐Ÿ”ฅ

๐Ÿ”ฅ Stop Writing Boilerplate! How Formik + React Hooks Can Save You 500 Hours This Year ๐Ÿ”ฅ

Introduction

Let's face it โ€“ form handling in React can be painful. As projects grow, developers often find themselves buried under piles of form validations, state management, and inconsistent user experiences.

You mightโ€™ve already heard of Formik โ€“ the popular form library for React. But what if I told you that most developers are underusing it? In this post, weโ€™re going to dive into a powerful, lesser-known combination: Formik + Custom React Hooks.

We'll explore how this pairing can:

  • Eliminate repetitive form boilerplate code ๐ŸŽฏ
  • Make your form components cleaner and more maintainable ๐Ÿ’…
  • Improve validation UX without breaking your brain ๐Ÿง 

Ready to save weeks editing form logic in 2024? Let's get our hands dirty. ๐Ÿ› ๏ธ


๐Ÿ’ฃ The Pain: Forms Before Formik (and Why Hooks Alone Arenโ€™t Enough)

A typical login form in plain React might look like this:

// LoginForm.jsx
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);

  const handleSubmit = e => {
    e.preventDefault();
    if (!email || !password) {
      setError('All fields are required.');
      return;
    }
    // Call API or authenticate...
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      {error && <p>{error}</p>}
      <button type="submit">Login</button>
    </form>
  );
}

export default LoginForm;
Enter fullscreen mode Exit fullscreen mode

Not terrible, but this is just login. Doing even basic validation across five different forms? Youโ€™re duplicating logic like a robot.


๐Ÿš€ Enter Formik + Yup: The Validation Dream Team

Formik simplifies form state and validation management. Pairing it with Yup, a JS schema validator, adds declarative, composable validation.

Letโ€™s rewrite our login form the Formik way:

// LoginFormWithFormik.jsx
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

const LoginSchema = Yup.object().shape({
  email: Yup.string().email('Invalid email').required('Required'),
  password: Yup.string().min(6, 'Too short').required('Required'),
});

function LoginForm() {
  const handleSubmit = (values) => {
    console.log('Submitted:', values);
    // Call API
  };

  return (
    <Formik
      initialValues={{ email: '', password: '' }}
      validationSchema={LoginSchema}
      onSubmit={handleSubmit}
    >
      <Form>
        <Field name="email" type="email" />
        <ErrorMessage name="email" component="div" />

        <Field name="password" type="password" />
        <ErrorMessage name="password" component="div" />

        <button type="submit">Login</button>
      </Form>
    </Formik>
  );
}

export default LoginForm;
Enter fullscreen mode Exit fullscreen mode

โœ… Zero manual useState management

โœ… Disables submit on validation errors

โœ… Clean, declarative code


๐Ÿง  Superpower Combo: Custom Hooks + Formik Context

What if you need to reuse business logic across forms? Create a custom hook to encapsulate logic!

Letโ€™s Make a useLoginForm Hook

// hooks/useLoginForm.js
import * as Yup from 'yup';

export const useLoginForm = () => {
  const initialValues = { email: '', password: '' };

  const validationSchema = Yup.object({
    email: Yup.string().email('Bad email').required('Required'),
    password: Yup.string().min(6, 'Minimum 6 characters').required('Required'),
  });

  const onSubmit = (values, { setSubmitting }) => {
    console.log('Login:', values);
    // ...API call
    setSubmitting(false);
  };

  return { initialValues, validationSchema, onSubmit };
};
Enter fullscreen mode Exit fullscreen mode

Now update your form to use this hook:

import { Formik, Form, Field, ErrorMessage } from 'formik';
import { useLoginForm } from './hooks/useLoginForm';

function LoginForm() {
  const form = useLoginForm();

  return (
    <Formik {...form}>
      <Form>
        <Field name="email" type="email" />
        <ErrorMessage name="email" component="div" />

        <Field name="password" type="password" />
        <ErrorMessage name="password" component="div" />

        <button type="submit">Login</button>
      </Form>
    </Formik>
  );
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงผ Clean, donโ€™t repeat yourself (DRY), fully reusable.

You could easily turn useLoginForm into useFormBuilder(type) and build a dynamic, unified form system.


๐Ÿ‘€ Bonus: Real-Time UX Improvements with useField Hook

Formik also lets you access field state via hooks:

import { useField } from 'formik';

function FancyEmailInput() {
  const [field, meta] = useField('email');

  return (
    <div>
      <input {...field} type="email" placeholder="What's your email?" />
      {meta.touched && meta.error && (
        <span className="error">{meta.error}</span>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use this pattern to build entire design systems that interface seamlessly with Formik.


โœจ Takeaways: Use these patterns today

Few things can instantly 10x DX (Developer Experience) like having:

  • A consistent form logic template via custom hooks
  • Declarative validation thatโ€™s testable
  • Components that separate UX and logic cleanly

In teams, this pays off instantly by reducing onboarding time, PR reviews, and logic bugs inside forms.


๐Ÿงช TLDR โ€“ Code Snippets Crash Course

Hereโ€™s what we covered:

โœ… Plain Form โ†’ Boilerplate

โœ… Formik + Yup โ†’ Cleaner, validatable code

โœ… Custom Hooks + Formik โ†’ Scalable, reusable logic

โœ… useField โ†’ Power up custom components


๐Ÿ’ก Pro Tip: Add Formik Debug Tools

<pre>{JSON.stringify(values, null, 2)}</pre>
<pre>{JSON.stringify(errors, null, 2)}</pre>
Enter fullscreen mode Exit fullscreen mode

Add them below forms in dev mode for ultra-fast debugging.


๐Ÿงญ Where to Learn More


๐Ÿ‘‹ Final Word

Forms donโ€™t have to be painful. With the Formik + React Hooks combo, you'll write less code, have fewer bugs, and stop pulling your hair out.

Start using these patterns today and you just might earn back ~500 hours this year โ€” or at least your front-end sanity ๐Ÿ˜‰.

๐Ÿ’ฌ Have a juicy form horror story? Share it in the comments below!


๐Ÿ› ๏ธ Tags: react, formik, web-development, productivity

โœจ If you need this done โ€“ we offer frontend development services!

Top comments (2)

Collapse
 
richmirks profile image
Richard Mirks

Quick clarification: you mention โ€œDisables submit on validation errors,โ€ but the Formik example doesn't disable the button. Are you using isValid/isSubmitting to set disabled on the submit, or is there another pattern you recommend? A small snippet would help.

Collapse
 
ekwoster profile image
Yevhen Kozachenko ๐Ÿ‡บ๐Ÿ‡ฆ

Good catch โ€” youโ€™re right, the example doesnโ€™t disable submit out of the box. The usual pattern is to wire up isValid and isSubmitting from Formikโ€™s render props or hooks. For example:

<Formik {...form}>
  {({ isValid, isSubmitting }) => (
    <Form>
      <Field name="email" type="email" />
      <ErrorMessage name="email" component="div" />

      <Field name="password" type="password" />
      <ErrorMessage name="password" component="div" />

      <button type="submit" disabled={!isValid || isSubmitting}>
        Login
      </button>
    </Form>
  )}
</Formik>
Enter fullscreen mode Exit fullscreen mode

That way the button stays disabled until the form is valid and not currently submitting.