6 min read

Validation with Formik/React Hook Form

Validation with Formik/React Hook Form

React forms need robust validation to ensure data quality in real-world apps like order management systems. This article explores Formik and React Hook Form libraries, building on controlled inputs and custom hooks from prior tutorials with advanced, copy-paste-ready examples.

Files to Create/Update

Create these files in your React project's src folder (use npx create-react-app validation-forms if starting fresh):

  • src/App.js – Main app integrating all examples.
  • src/OrderFormFormik.js – Formik-based order form with validation.
  • src/OrderFormHookForm.js – React Hook Form version.
  • src/OrderList.js – Reusable list component (improved from prior articles).
  • package.json updates: Run npm install formik yup @hookform/resolvers react-hook-form yup after setup.

Full App.js and component code provided below – copy-paste, npm start, and test instantly.

Why Form Validation Matters

Validation prevents invalid data like negative order amounts or empty customer names, extending controlled vs uncontrolled inputs. Manual checks with useState work for basics but scale poorly; libraries like Formik (schema-driven, batteries-included) and React Hook Form (minimal, performant) handle complexity.

These build on props vs state by lifting errors to parent state and custom hooks for reusable logic.

Installing Dependencies

In your project root:

npm install formik yup react-hook-form @hookform/resolvers/yup

Formik uses Yup for schemas; React Hook Form integrates it similarly. Restart npm start after install.

Formik simplifies forms with built-in state, validation, and submission. It tracks valueserrorstouched, and isSubmitting automatically.

Basic Order Form with Formik

Extend the order form from controlled inputs: validate customer (required, min length) and amount (required, >0).

src/OrderFormFormik.js

import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

const validationSchema = Yup.object({
  customer: Yup.string().required('Customer name required').min(2, 'At least 2 characters'),
  amount: Yup.number().required('Amount required').positive('Must be positive').min(10, 'Minimum $10'),
  notes: Yup.string().max(100, 'Notes too long')
});

function OrderFormFormik({ onCreateOrder }) {
  return (
    <Formik
      initialValues={{ customer: '', amount: '', notes: '' }}
      validationSchema={validationSchema}
      onSubmit={(values, { resetForm }) => {
        onCreateOrder({ ...values, amount: Number(values.amount) });
        resetForm();
      }}
    >
      {({ isSubmitting }) => (
        <Form>
          <label>
            Customer Name:
            <Field name="customer" type="text" />
            <ErrorMessage name="customer" component="div" style={{ color: 'red' }} />
          </label>
          <br />
          <label>
            Order Amount:
            <Field name="amount" type="number" />
            <ErrorMessage name="amount" component="div" style={{ color: 'red' }} />
          </label>
          <br />
          <label>
            Notes:
            <Field name="notes" as="textarea" />
            <ErrorMessage name="notes" component="div" style={{ color: 'red' }} />
          </label>
          <br />
          <button type="submit" disabled={isSubmitting}>
            Create Order
          </button>
        </Form>
      )}
    </Formik>
  );
}

export default OrderFormFormik;

Key Features:

  • Field auto-wires valueonChangeonBlur.
  • ErrorMessage shows Yup errors only on touched fields.
  • resetForm() clears after submit, improving UX over manual useState resets.

Integrating with App State

Use props for parent-child communication, per parent-child communication.

src/OrderList.js (Reusable from keys in lists)

import React from 'react';

function OrderList({ orders }) {
  return (
    <ul>
      {orders.map((order) => (
        <li key={order.id}>
          #{order.id}: {order.customer} — ${order.amount} ({order.notes || 'No notes'})
        </li>
      ))}
    </ul>
  );
}

export default OrderList;

src/App.js (Complete, switchable demo)

import React, { useState } from 'react';
import OrderFormFormik from './OrderFormFormik';
import OrderFormHookForm from './OrderFormHookForm'; // We'll add this next
import OrderList from './OrderList';

function App() {
  const [orders, setOrders] = useState([]);
  const [activeForm, setActiveForm] = useState('formik'); // Toggle: 'formik' or 'hookform'

  const handleCreateOrder = (order) => {
    const newOrder = { id: Date.now(), ...order };
    setOrders([newOrder, ...orders]);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Order Management with Validation</h1>
      <button onClick={() => setActiveForm('formik')}>Formik Form</button>
      <button onClick={() => setActiveForm('hookform')}>React Hook Form</button>
      <h2>{activeForm === 'formik' ? 'Formik' : 'React Hook Form'}</h2>
      {activeForm === 'formik' ? (
        <OrderFormFormik onCreateOrder={handleCreateOrder} />
      ) : (
        <OrderFormHookForm onCreateOrder={handleCreateOrder} />
      )}
      <h3>Orders</h3>
      <OrderList orders={orders} />
    </div>
  );
}

export default App;

React Hook Form: Lightweight Alternative

React Hook Form uses refs under the hood (uncontrolled-style) for fewer re-renders, ideal for perf-sensitive apps. Integrates Yup via resolver.

Advanced Order Form with Hook Form

Builds on Formik but adds useWatch for real-time totals (ties to useMemo).

src/OrderFormHookForm.js

import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

const validationSchema = Yup.object({
  customer: Yup.string().required('Customer name required').min(2, 'At least 2 characters'),
  amount: Yup.number().required('Amount required').positive('Must be positive').min(10, 'Minimum $10'),
  discount: Yup.number().min(0, 'Discount >=0').max(50, 'Max 50%'),
  notes: Yup.string().max(100, 'Notes too long')
});

function OrderFormHookForm({ onCreateOrder }) {
  const { register, handleSubmit, formState: { errors, isSubmitting }, watch, reset } = useForm({
    resolver: yupResolver(validationSchema),
    defaultValues: { customer: '', amount: '', discount: 0, notes: '' }
  });

  const amount = watch('amount');
  const discount = watch('discount') || 0;
  const total = amount ? (Number(amount) * (1 - discount / 100)).toFixed(2) : 0;

  const onSubmit = (data) => {
    onCreateOrder({ ...data, amount: Number(data.amount), discount: Number(data.discount) });
    reset();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>
        Customer Name:
        <input {...register('customer')} />
        {errors.customer && <div style={{ color: 'red' }}>{errors.customer.message}</div>}
      </label>
      <br />
      <label>
        Order Amount:
        <input type="number" {...register('amount')} />
        {errors.amount && <div style={{ color: 'red' }}>{errors.amount.message}</div>}
      </label>
      <br />
      <label>
        Discount (%):
        <input type="number" {...register('discount')} />
        {errors.discount && <div style={{ color: 'red' }}>{errors.discount.message}</div>}
      </label>
      <br />
      <div>Final Total: ${total}</div>
      <label>
        Notes:
        <textarea {...register('notes')} />
        {errors.notes && <div style={{ color: 'red' }}>{errors.notes.message}</div>}
      </label>
      <br />
      <button type="submit" disabled={isSubmitting}>Create Order</button>
    </form>
  );
}

export default OrderFormHookForm;

Advantages Over Formik:

  • Fewer re-renders (uses native validation).
  • watch enables reactive UI like live totals without useEffect pitfalls.
  • Smaller bundle (~9kb vs Formik's 15kb).

Formik vs React Hook Form Comparison

FeatureFormikReact Hook Form
RendersControlled (more re-renders)Uncontrolled refs (perf)
ValidationBuilt-in YupResolver (Yup, Zod, etc.)
Bundle SizeLarger (state-heavy)Minimal
Learning CurveOpinionated APIHook-based, flexible
Best ForComplex forms, batteries-includedHigh-perf, simple validation

Test in App.js – Hook Form updates total live without lag.

Complex Example: Product Order with List

Advance lists/keys + reusable components: Formik form for multi-product order.

src/ProductOrderForm.js

import React from 'react';
import { Formik, Form, FieldArray, Field } from 'formik';
import * as Yup from 'yup';

const schema = Yup.object({
  customer: Yup.string().required(),
  products: Yup.array().of(
    Yup.object({
      name: Yup.string().required(),
      qty: Yup.number().required().min(1),
      price: Yup.number().required().min(0.01)
    })
  )
});

function ProductOrderForm({ onCreateOrder }) {
  return (
    <Formik
      initialValues={{ customer: '', products: [{ name: '', qty: 1, price: 0 }] }}
      validationSchema={schema}
      onSubmit={onCreateOrder}
    >
        {({ values }) => (
        <Form>
            <label>Customer: <Field name="customer" /></label>
            <FieldArray name="products">
                {({ push, remove }) => (
                <div>
                    {values.products.map((product, index) => (
                    <div key={index}>
                        <Field name={`products.${index}.name`} placeholder="Product" />
                        <Field name={`products.${index}.qty`} type="number" />
                        <Field name={`products.${index}.price`} type="number" />
                        <button type="button" onClick={() => remove(index)}>Remove</button>
                    </div>
                    ))}
                    <button type="button" onClick={() => push({ name: '', qty: 1, price: 0 })}>
                    Add Product
                    </button>              
                    </div>
                )}
            </FieldArray>
            <button type="submit">Submit Order</button>
        </Form>
        )}
    </Formik>
)}

export default ProductOrderForm;

Update App.js to include: <ProductOrderForm onCreateOrder={(data) => setOrders([data, ...orders])} />. Handles dynamic arrays with stable keys.

Error Handling Pitfalls

  • Formik: touched prevents early errors; use validateOnMount carefully.
  • Hook Form: mode: 'onChange' for real-time, but pair with debounce for async.
  • Shared: Lift state up for global errors, per lifting state.

Performance Tips

Memoize heavy forms with React.memo/useCallback:

const MemoForm = React.memo(OrderFormFormik);

For lists >100 items, use useMemo on totals.

Local vs Global State in Forms

Simple: Local (useForm). Complex (multi-page): Context/Redux, from local vs global.

Conclusion

Formik suits structured apps; React Hook Form excels in speed. Both elevate controlled inputs to production-ready. Experiment with provided code – extend to your order dashboard.

Next article: Multi-step forms and wizards.