Building Type-Safe React Forms: Beyond useState

Building Type-Safe React Forms: Beyond useState

Ihor (Harry) Chyshkala
Ihor (Harry) ChyshkalaAuthor
|2 min read

Building forms in React can quickly become complex, especially when dealing with validation, error handling, and type safety. In this article, we'll explore how to create a robust, type-safe form system that goes beyond simple useState implementations.

The Problem with Traditional Form Approaches

Let's start with a common way of handling forms in React:

typescript(23 lines)
1function UserForm() {
2  const [name, setName] = useState('');
3  const [email, setEmail] = useState('');
4  const [age, setAge] = useState('');
5  const [errors, setErrors] = useState<Record<string, string>>({});
6
7  const handleSubmit = (e: React.FormEvent) => {
8    e.preventDefault();

This approach has several problems:

  1. No type safety for form values
  2. Repetitive state declarations
  3. Validation logic mixed with UI
  4. No centralized form state
  5. Hard to reuse validation logic

Building a Type-Safe Form Hook

Let's create a custom hook that addresses these issues:

typescript(114 lines)
1interface FormField<T> {
2  value: T;
3  error?: string;
4  touched: boolean;
5  validators?: ((value: T) => string | undefined)[];
6}
7
8type FormState<T> = {

Creating Reusable Validators

Let's create some type-safe validators:

typescript(32 lines)
1const createValidator = <T>(
2  predicate: (value: T) => boolean,
3  errorMessage: string
4) => {
5  return (value: T): string | undefined => 
6    predicate(value) ? undefined : errorMessage;
7};
8

Using the Form Hook

Here's how to use our type-safe form system:

typescript(66 lines)
1interface UserFormData {
2  name: string;
3  email: string;
4  age: string;
5}
6
7function UserForm() {
8  const {

Creating Reusable Form Fields

To reduce boilerplate even further, let's create a reusable form field component:

typescript(29 lines)
1interface FormFieldProps<T, K extends keyof T> {
2  name: K;
3  label: string;
4  formState: FormState<T>;
5  setFieldValue: (field: K, value: T[K]) => void;
6}
7
8function FormField<T, K extends keyof T>({

Now our form becomes much cleaner:

typescript(42 lines)
1function UserForm() {
2  const form = useForm<UserFormData>(
3    {
4      name: '',
5      email: '',
6      age: '',
7    },
8    async (values) => {

Advanced Features

Here are some additional features we could add to our form system:

1. Form-Level Validation

typescript
1interface FormValidation<T> {
2  (values: T): Partial<Record<keyof T, string>> | undefined;
3}
4
5// Add to useForm parameters
6formValidation?: FormValidation<T>

2. Async Validation

typescript
1const asyncValidator = <T>(
2  validate: (value: T) => Promise<string | undefined>
3) => {
4  return async (value: T): Promise<string | undefined> => {
5    return await validate(value);
6  };
7};
8
9// Example usage
10const uniqueEmailValidator = asyncValidator(async (email: string) => {
11  const response = await checkEmailExists(email);
12  return response.exists ? 'Email already exists' : undefined;
13});

3. Field Dependencies

typescript
1interface FieldDependency<T, K extends keyof T> {
2  fields: K[];
3  validator: (values: Pick<T, K>) => string | undefined;
4}

Performance Considerations

  1. Memoization: Our hook uses useCallback for functions to prevent unnecessary rerenders.
  2. Selective Updates: The form state updates only the changed fields, not the entire form state.
  3. Validation Optimization: Validation runs only on touched fields or during form submission.

Testing Your Forms

Here's how to test your type-safe forms:

typescript(33 lines)
1import { renderHook, act } from '@testing-library/react-hooks';
2
3describe('useForm', () => {
4  it('should initialize with correct values', () => {
5    const { result } = renderHook(() =>
6      useForm(
7        { name: '', email: '' },
8        () => {}

Conclusion

Building type-safe forms in React requires careful consideration of:

  • State management
  • Validation
  • Type safety
  • Reusability
  • Performance

Our solution provides:

  • Full type safety with TypeScript
  • Reusable validation logic
  • Centralized form state
  • Performance optimizations
  • Easy testing capabilities

Remember that while this implementation is quite robust, you might want to consider existing form libraries like Formik or React Hook Form for production applications. However, understanding these patterns helps you make better decisions about when to use existing solutions versus building custom implementations.

About the Author

Ihor (Harry) Chyshkala

Ihor (Harry) Chyshkala

Code Alchemist: Transmuting Ideas into Reality with JS & PHP. DevOps Wizard: Transforming Infrastructure into Cloud Gold | Orchestrating CI/CD Magic | Crafting Automation Elixirs