DEV Community

Cover image for Building Dependent Form Fields in React: A Practical Approach
kensaadi
kensaadi

Posted on

Building Dependent Form Fields in React: A Practical Approach

Have you ever needed a form where changing one field updates the options of another? Like selecting a category and seeing subcategories automatically update?

Most developers reach for useEffect + manual state management. But there's a better way.

The Problem

Let's say you have two autocomplete fields:

  • Category - static options
  • Subcategory - depends on category, loads dynamically

The traditional approach requires:

  • Tracking category changes
  • Triggering updates when category changes
  • Manual state management for loading/error states
  • Clearing subcategory when category changes
  • Managing async operations

It gets complex fast when you add more dependencies.

The DashForge Solution

DashForge uses reactions - a declarative way to handle field dependencies:

import { useState } from "react";
import { DashForm } from "@dashforge/forms";
import { Autocomplete } from "@dashforge/ui";
import { Box, Button, Card, Stack, Typography } from "@mui/material";

interface FormValues {
  category: string | null;
  subcategory: string | null;
}

const categories = ["Electronics", "Clothing", "Books"];

const subcategoriesByCategory: Record = {
  Electronics: ["Phones", "Laptops", "Tablets", "Accessories"],
  Clothing: ["Shirt", "Pants", "Shoes", "Dresses"],
  Books: ["Fiction", "Non-Fiction", "Science", "History"],
};

async function fetchSubcategories(category: string | null) {
  await new Promise((resolve) => setTimeout(resolve, 500));
  return category ? subcategoriesByCategory[category] || [] : [];
}

export function DependencyForm() {
  const [formData, setFormData] = useState(null);

  const onSubmit = (data: FormValues) => {
    setFormData(data);
  };

  return (
        <DashForm
          onSubmit={onSubmit}
          defaultValues={{ category: null, subcategory: null }}
          reactions={[
            {
              id: "update-subcategory-options",
              watch: ["category"],
              run: async ({ getValue, setRuntime }) => {
                const category = getValue("category");

                setRuntime("subcategory", {
                  status: "loading",
                  error: null,
                  data: { options: [] },
                });

                const subcategories = await fetchSubcategories(category);

                setRuntime("subcategory", {
                  status: "ready",
                  error: null,
                  data: { options: subcategories },
                });
              },
            },
          ]}
        >
            Submit
        {formData && (
            {JSON.stringify(formData, null, 2)}
        )}
  );
}
Enter fullscreen mode Exit fullscreen mode

How It Works

The key is the reactions array:

  1. watch - Specify which fields to monitor
  2. run - When watched fields change, execute this function
  3. getValue - Read current field values from the form
  4. setRuntime - Update field data at runtime (options, loading state, errors)

When category changes:

  1. Reaction triggers automatically
  2. Fetch new subcategories
  3. Update subcategory field with new options
  4. User sees updated options immediately

Why This Approach is Better

No Manual Side Effects

The form handles dependencies automatically. No need for useEffect or manual state management.

Clean Dependencies

Just declare what you're watching with the watch array. No complex dependency tracking.

Loading States Built-in

Set status: "loading" and the field knows it's fetching. Built-in support for async operations.

Type-Safe

Full TypeScript support for getValue and setRuntime.

Scales Well

Add another dependent field? Add another reaction. Complexity stays linear, not exponential.

Advanced: Multiple Dependencies

What if subcategory affects another field? Just add another reaction:

reactions={[
  {
    id: "update-subcategory-options",
    watch: ["category"],
    run: async ({ getValue, setRuntime }) => {
      // Update subcategory based on category
    },
  },
  {
    id: "update-product-options",
    watch: ["subcategory"],
    run: async ({ getValue, setRuntime }) => {
      // Update product based on subcategory
    },
  },
]}
Enter fullscreen mode Exit fullscreen mode

The form orchestrates the entire dependency chain.

Real-World Scenarios

This pattern works for:

  • Cascading dropdowns - Country → State → City
  • Dynamic pricing - Product → Variant → Price
  • Conditional visibility - Type → Related options
  • Async data loading - Search field → fetch results
  • Complex workflows - Multi-step forms with dependencies

Getting Started

Install DashForge:

npm install @dashforge/forms @dashforge/ui
Enter fullscreen mode Exit fullscreen mode

See the full documentation for API details.

Check out the starter kits for production examples.

The Point

Dependent form fields don't need to be complex. With the right abstraction, they become straightforward: declare what you're watching, declare what updates, let the form handle the rest

Top comments (0)