Skip to main content

React Final Form

Introduction

React Final Form is a popular form management library for React that focuses on performance, simplicity, and powerful validation capabilities. Built by Erik Rasmussen (the same author who created Redux Form), React Final Form provides an efficient solution for handling complex form states in React applications without the Redux dependency.

This library embraces a subscription-based model that helps minimize re-renders, making it extremely performant even for large forms with many fields. In this tutorial, we'll explore React Final Form's core concepts and see how it can simplify form handling in your React applications.

Getting Started with React Final Form

Installation

First, let's install the required packages:

bash
npm install final-form react-final-form
# or with yarn
yarn add final-form react-final-form

React Final Form consists of two main packages:

  • final-form: The core library that handles form state management
  • react-final-form: React bindings for Final Form

Core Components

React Final Form provides several key components:

  1. Form: The main container component that creates a Final Form instance
  2. Field: Component to connect form fields to Final Form
  3. FormSpy: Component for observing form state without causing re-renders

Basic Form Example

Let's create a simple login form to understand the basics:

jsx
import React from 'react';
import { Form, Field } from 'react-final-form';

const LoginForm = () => {
const onSubmit = (values) => {
// In a real app, you would handle login here
console.log('Form submitted with:', values);
};

return (
<Form
onSubmit={onSubmit}
render={({ handleSubmit, form, submitting, pristine }) => (
<form onSubmit={handleSubmit}>
<div>
<label>Username</label>
<Field
name="username"
component="input"
type="text"
placeholder="Username"
/>
</div>

<div>
<label>Password</label>
<Field
name="password"
component="input"
type="password"
placeholder="Password"
/>
</div>

<div className="buttons">
<button type="submit" disabled={submitting}>
Log In
</button>
<button
type="button"
onClick={form.reset}
disabled={submitting || pristine}
>
Reset
</button>
</div>
</form>
)}
/>
);
};

export default LoginForm;

When this form is submitted, the onSubmit function receives the form values as an object:

javascript
// Example output when form is submitted
{
username: "user123",
password: "myPassword"
}

Form Validation

One of React Final Form's strengths is its validation system. You can validate forms in several ways:

1. Simple Validation

jsx
import React from 'react';
import { Form, Field } from 'react-final-form';

const validate = values => {
const errors = {};

if (!values.username) {
errors.username = 'Required';
}

if (!values.password) {
errors.password = 'Required';
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}

return errors;
};

const LoginFormWithValidation = () => {
const onSubmit = values => {
console.log('Form submitted with:', values);
};

return (
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit, submitting }) => (
<form onSubmit={handleSubmit}>
<div>
<label>Username</label>
<Field name="username">
{({ input, meta }) => (
<div>
<input {...input} type="text" placeholder="Username" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
</div>

<div>
<label>Password</label>
<Field name="password">
{({ input, meta }) => (
<div>
<input {...input} type="password" placeholder="Password" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
</div>

<button type="submit" disabled={submitting}>
Log In
</button>
</form>
)}
/>
);
};

2. Field-Level Validation

You can also validate each field individually:

jsx
// Field-level validation example
<Field
name="email"
validate={value => {
if (!value) return 'Required';
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) {
return 'Invalid email address';
}
return undefined;
}}
>
{({ input, meta }) => (
<div>
<input {...input} type="text" placeholder="Email" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>

Custom Field Components

React Final Form allows for easy creation of custom field components:

jsx
// CustomInput.jsx
import React from 'react';
import { Field } from 'react-final-form';

const CustomInput = ({ name, label, type = 'text', placeholder }) => (
<Field name={name}>
{({ input, meta }) => (
<div className="form-group">
<label>{label}</label>
<input
{...input}
type={type}
placeholder={placeholder}
className={meta.error && meta.touched ? 'error' : ''}
/>
{meta.error && meta.touched && (
<span className="error-message">{meta.error}</span>
)}
</div>
)}
</Field>
);

export default CustomInput;

Then you can use it in your form:

jsx
import React from 'react';
import { Form } from 'react-final-form';
import CustomInput from './CustomInput';

const ProfileForm = () => {
const onSubmit = values => {
console.log(values);
};

return (
<Form
onSubmit={onSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<CustomInput
name="firstName"
label="First Name"
placeholder="Enter your first name"
/>
<CustomInput
name="lastName"
label="Last Name"
placeholder="Enter your last name"
/>
<CustomInput
name="email"
type="email"
label="Email"
placeholder="Enter your email"
/>
<button type="submit">Save Profile</button>
</form>
)}
/>
);
};

Form Submission and State Management

React Final Form provides useful form state information via the render props pattern:

jsx
<Form
onSubmit={onSubmit}
render={({
handleSubmit,
form,
submitting,
pristine,
values,
valid,
errors,
// many more properties available
}) => (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<div>
<button type="submit" disabled={submitting || !valid}>
Submit
</button>
<button type="button" onClick={form.reset} disabled={submitting || pristine}>
Reset
</button>
</div>

{/* Show current form values (useful for debugging) */}
<pre>{JSON.stringify(values, undefined, 2)}</pre>
</form>
)}
/>

Handling Submission

Let's look at a more realistic submission example:

jsx
import React, { useState } from 'react';
import { Form, Field } from 'react-final-form';

const RegistrationForm = () => {
const [serverError, setServerError] = useState(null);
const [submitSucceeded, setSubmitSucceeded] = useState(false);

const onSubmit = async (values) => {
// Reset previous errors
setServerError(null);

try {
// Simulating an API call
await new Promise(resolve => setTimeout(resolve, 1000));

// Check for duplicate email (simulated)
if (values.email === '[email protected]') {
throw new Error('Email already in use');
}

console.log('Registration successful:', values);
setSubmitSucceeded(true);
} catch (error) {
setServerError(error.message);
return { [FORM_ERROR]: error.message }; // Special FORM_ERROR key
}
};

if (submitSucceeded) {
return <div>Registration successful! Please check your email.</div>;
}

return (
<Form
onSubmit={onSubmit}
validate={validateRegistrationForm}
render={({ handleSubmit, submitting, submitError }) => (
<form onSubmit={handleSubmit}>
{serverError && <div className="error">{serverError}</div>}
{submitError && <div className="error">{submitError}</div>}

<Field name="fullName">
{({ input, meta }) => (
<div>
<label>Full Name</label>
<input {...input} type="text" placeholder="Full Name" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>

<Field name="email">
{({ input, meta }) => (
<div>
<label>Email</label>
<input {...input} type="email" placeholder="Email" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>

<Field name="password">
{({ input, meta }) => (
<div>
<label>Password</label>
<input {...input} type="password" placeholder="Password" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>

<button type="submit" disabled={submitting}>
{submitting ? 'Registering...' : 'Register'}
</button>
</form>
)}
/>
);
};

// Validation function
const validateRegistrationForm = values => {
const errors = {};
if (!values.fullName) errors.fullName = 'Required';
if (!values.email) {
errors.email = 'Required';
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
errors.email = 'Invalid email address';
}
if (!values.password) {
errors.password = 'Required';
} else if (values.password.length < 8) {
errors.password = 'Must be at least 8 characters';
}
return errors;
};

Dynamic Forms with Arrays

React Final Form can handle dynamic forms, including arrays of fields:

jsx
import React from 'react';
import { Form, Field } from 'react-final-form';
import { FieldArray } from 'react-final-form-arrays';
import arrayMutators from 'final-form-arrays';

const OrderForm = () => {
const onSubmit = values => {
console.log('Order submitted:', values);
};

return (
<Form
onSubmit={onSubmit}
mutators={{
...arrayMutators
}}
initialValues={{ items: [{ product: '', quantity: 1 }] }}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}>
<h2>Order Form</h2>

<FieldArray name="items">
{({ fields }) => (
<div>
<button
type="button"
onClick={() => fields.push({ product: '', quantity: 1 })}
>
Add Item
</button>

{fields.map((name, index) => (
<div key={name} className="item-row">
<h4>Item #{index + 1}</h4>

<Field
name={`${name}.product`}
component="select"
>
{({ input }) => (
<div>
<label>Product</label>
<select {...input}>
<option value="">Select a product...</option>
<option value="product1">Product 1</option>
<option value="product2">Product 2</option>
<option value="product3">Product 3</option>
</select>
</div>
)}
</Field>

<Field
name={`${name}.quantity`}
type="number"
>
{({ input, meta }) => (
<div>
<label>Quantity</label>
<input {...input} min="1" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>

<button
type="button"
onClick={() => fields.remove(index)}
>
Remove
</button>
</div>
))}
</div>
)}
</FieldArray>

<div className="buttons">
<button type="submit" disabled={submitting}>
Submit Order
</button>
<button
type="button"
onClick={form.reset}
disabled={submitting || pristine}
>
Reset
</button>
</div>

<pre>{JSON.stringify(values, 0, 2)}</pre>
</form>
)}
/>
);
};

Using FormSpy for Performance

FormSpy allows you to monitor form state without causing unnecessary re-renders:

jsx
import React from 'react';
import { Form, Field, FormSpy } from 'react-final-form';

const FormWithSpy = () => {
return (
<Form
onSubmit={values => console.log(values)}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div>
<label>First Name</label>
<Field name="firstName" component="input" placeholder="First Name" />
</div>

<div>
<label>Last Name</label>
<Field name="lastName" component="input" placeholder="Last Name" />
</div>

<button type="submit">Submit</button>

{/* The FormSpy only updates when form values change */}
<FormSpy subscription={{ values: true }}>
{({ values }) => (
<div className="form-preview">
<h3>Form Preview:</h3>
<p>First Name: {values.firstName || '(empty)'}</p>
<p>Last Name: {values.lastName || '(empty)'}</p>
</div>
)}
</FormSpy>
</form>
)}
/>
);
};

Real-World Example: Multi-Step Form

Here's a more complex example showing a multi-step form:

jsx
import React, { useState } from 'react';
import { Form } from 'react-final-form';
import CustomInput from './CustomInput';

// Step components
const PersonalInfoStep = ({ next }) => (
<>
<h2>Personal Information</h2>
<CustomInput name="firstName" label="First Name" />
<CustomInput name="lastName" label="Last Name" />
<CustomInput name="email" label="Email" type="email" />
<button type="button" onClick={next}>Next</button>
</>
);

const AddressStep = ({ next, previous }) => (
<>
<h2>Address Information</h2>
<CustomInput name="streetAddress" label="Street Address" />
<CustomInput name="city" label="City" />
<CustomInput name="state" label="State" />
<CustomInput name="zip" label="Zip Code" />
<div>
<button type="button" onClick={previous}>Back</button>
<button type="button" onClick={next}>Next</button>
</div>
</>
);

const ReviewStep = ({ previous, values }) => (
<>
<h2>Review Your Information</h2>
<div className="review-section">
<h3>Personal Info</h3>
<p><strong>Name:</strong> {values.firstName} {values.lastName}</p>
<p><strong>Email:</strong> {values.email}</p>
</div>
<div className="review-section">
<h3>Address</h3>
<p><strong>Street:</strong> {values.streetAddress}</p>
<p><strong>City:</strong> {values.city}, {values.state} {values.zip}</p>
</div>
<div>
<button type="button" onClick={previous}>Back</button>
<button type="submit">Submit</button>
</div>
</>
);

// Main component
const MultiStepForm = () => {
const [step, setStep] = useState(0);

const next = () => setStep(step + 1);
const previous = () => setStep(step - 1);

const onSubmit = async (values) => {
// In a real app, submit to an API here
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Form submitted successfully!\n\n' + JSON.stringify(values, null, 2));
};

return (
<div className="multi-step-form">
<Form
onSubmit={onSubmit}
initialValues={{
firstName: '',
lastName: '',
email: '',
streetAddress: '',
city: '',
state: '',
zip: ''
}}
render={({ handleSubmit, values, submitting }) => (
<form onSubmit={handleSubmit}>
{step === 0 && <PersonalInfoStep next={next} />}
{step === 1 && <AddressStep next={next} previous={previous} />}
{step === 2 && <ReviewStep previous={previous} values={values} />}

<div className="progress-indicator">
Step {step + 1} of 3
</div>
</form>
)}
/>
</div>
);
};

Best Practices

When working with React Final Form, consider these best practices:

  1. Use subscription prop for optimization: Only subscribe to the field and form state that you actually need

    jsx
    <Field name="username" subscription={{ value: true, error: true, touched: true }}>
    {/* ... */}
    </Field>
  2. Separate validation logic: Keep your validation functions separate from your component code

  3. Reuse field components: Create custom field components to maintain consistent UI and behavior

  4. Use initialValues prop instead of manually setting values

  5. Use FormSpy sparingly: It's powerful but can lead to performance issues if overused

Summary

React Final Form is a powerful and flexible form management library that provides:

  • High performance through its subscription model
  • Comprehensive form validation capabilities
  • Excellent developer experience with minimal boilerplate
  • Support for complex form scenarios like dynamic fields and multi-step forms

The library's focus on performance and simplicity makes it an excellent choice for both simple forms and complex form scenarios in React applications.

Additional Resources

To deepen your knowledge of React Final Form:

  1. Official React Final Form Documentation
  2. Final Form GitHub Repository
  3. React Final Form GitHub Repository

Practice Exercises

  1. Create a registration form with the following fields:

    • Username (required)
    • Email (required, valid email format)
    • Password (required, minimum 8 characters)
    • Confirm Password (must match password)
  2. Build a dynamic form that allows users to add multiple education entries (school name, degree, graduation year)

  3. Create a payment form with credit card validation and different payment method options

  4. Implement a form with conditional fields (e.g., show additional fields based on selections)



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)