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.