Quick Start
Build your first dynamic form with Dashforge in minutes.
Basic Setup
Three steps to get started
The Dashforge Form System requires minimal setup. You wrap your form in DashForm, define your fields, and optionally add reactions for dynamic behavior.
Step 1: Wrap your form
import { DashForm } from '@dashforge/ui';
function MyForm() {
return (
<DashForm onSubmit={(data) => console.log(data)}>
{/* Your form fields go here */}
</DashForm>
);
}Step 2: Add form fields
import { DashForm, TextField, Select } from '@dashforge/ui';
function MyForm() {
return (
<DashForm onSubmit={(data) => console.log(data)}>
<TextField
name="email"
label="Email"
rules={{ required: 'Email is required' }}
/>
<TextField
name="password"
label="Password"
type="password"
rules={{ required: 'Password is required' }}
/>
</DashForm>
);
}Step 3: Add dynamic behavior (optional)
To make your form dynamic, pass a reactions array to DashForm:
import { DashForm, TextField, Select } from '@dashforge/ui';
function AddressForm() {
const reactions = [
{
id: 'load-states',
watch: ['country'],
run: async (ctx) => {
const country = ctx.getValue<string>('country');
if (!country) {
ctx.setRuntime('state', { status: 'idle', data: null });
return;
}
const requestId = ctx.beginAsync('fetch-states');
ctx.setRuntime('state', { status: 'loading' });
const states = await fetchStatesByCountry(country);
if (ctx.isLatest('fetch-states', requestId)) {
ctx.setRuntime('state', {
status: 'ready',
data: { options: states }
});
}
}
}
];
return (
<DashForm
reactions={reactions}
onSubmit={(data) => console.log(data)}
>
<Select
name="country"
label="Country"
options={['United States', 'Canada', 'Mexico']}
rules={{ required: 'Country is required' }}
/>
<Select
name="state"
label="State / Province"
optionsFromFieldData
visibleWhen={(engine) =>
engine.getNode('country')?.value != null
}
rules={{ required: 'State is required' }}
/>
<TextField name="city" label="City" />
</DashForm>
);
}✓
That's it! You now have a form where the state dropdown automatically loads options when a country is selected, with loading states and stale response handling built in.
Complete Working Example
A realistic form with chained dependencies
This example shows what makes Dashforge powerful: three cascading dropdowns (Country → State → City), with async loading, automatic stale response protection, and conditional visibility—all without useEffect or manual state management.
import { DashForm, TextField, Select, Autocomplete } from '@dashforge/ui';
import { useState } from 'react';
// Mock API functions
async function fetchStates(country: string) {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 500));
const statesByCountry = {
'United States': ['California', 'Texas', 'New York', 'Florida'],
'Canada': ['Ontario', 'Quebec', 'British Columbia', 'Alberta'],
'Mexico': ['Jalisco', 'Nuevo León', 'Yucatán', 'Quintana Roo']
};
return statesByCountry[country as keyof typeof statesByCountry] || [];
}
async function fetchCities(state: string) {
await new Promise(resolve => setTimeout(resolve, 500));
const citiesByState: Record<string, string[]> = {
'California': ['Los Angeles', 'San Francisco', 'San Diego'],
'Texas': ['Houston', 'Austin', 'Dallas'],
'Ontario': ['Toronto', 'Ottawa', 'Hamilton'],
'Quebec': ['Montreal', 'Quebec City', 'Laval']
};
return citiesByState[state] || [];
}
export function DynamicAddressForm() {
const [submittedData, setSubmittedData] = useState<any>(null);
const reactions = [
{
id: 'load-states',
watch: ['country'],
run: async (ctx) => {
const country = ctx.getValue<string>('country');
if (!country) {
ctx.setRuntime('state', { status: 'idle', data: null });
return;
}
const requestId = ctx.beginAsync('fetch-states');
ctx.setRuntime('state', { status: 'loading' });
const states = await fetchStates(country);
if (ctx.isLatest('fetch-states', requestId)) {
ctx.setRuntime('state', {
status: 'ready',
data: { options: states }
});
}
}
},
{
id: 'load-cities',
watch: ['state'],
run: async (ctx) => {
const state = ctx.getValue<string>('state');
if (!state) {
ctx.setRuntime('city', { status: 'idle', data: null });
return;
}
const requestId = ctx.beginAsync('fetch-cities');
ctx.setRuntime('city', { status: 'loading' });
const cities = await fetchCities(state);
if (ctx.isLatest('fetch-cities', requestId)) {
ctx.setRuntime('city', {
status: 'ready',
data: { options: cities }
});
}
}
}
];
return (
<div>
<DashForm
reactions={reactions}
onSubmit={(data) => setSubmittedData(data)}
>
<Select
name="country"
label="Country"
options={['United States', 'Canada', 'Mexico']}
rules={{ required: 'Please select a country' }}
/>
<Select
name="state"
label="State / Province"
optionsFromFieldData
visibleWhen={(engine) =>
engine.getNode('country')?.value != null
}
rules={{ required: 'Please select a state' }}
/>
<Autocomplete
name="city"
label="City"
optionsFromFieldData
visibleWhen={(engine) =>
engine.getNode('state')?.value != null
}
rules={{ required: 'Please enter a city' }}
/>
<TextField
name="postalCode"
label="Postal Code"
visibleWhen={(engine) =>
engine.getNode('city')?.value != null
}
/>
<button type="submit">Submit</button>
</DashForm>
{submittedData && (
<div>
<h3>Submitted Data:</h3>
<pre>{JSON.stringify(submittedData, null, 2)}</pre>
</div>
)}
</div>
);
}This example demonstrates:
- Chained dependencies: Country → States → Cities
- Async data loading: Fetching options from APIs
- Loading states: Automatic loading indicators
- Stale response protection: Prevents race conditions
- Conditional visibility: Fields appear based on previous selections
- Form validation: Standard React Hook Form validation
ℹ️
Notice how the component code is clean and declarative. All the orchestration logic lives in the reactions array, not scattered throughout the component.
Key Concepts
Understanding what makes this work
Reactions
Declarative side effects that run when watched fields change. They can read values, fetch data, and update runtime state. The system handles execution timing and async coordination automatically.
Runtime State
Separate storage for field metadata like loading status, dynamic options, and async errors. This keeps your form values clean while providing rich UI feedback. Fields read from runtime state using optionsFromFieldData.
Conditional Visibility
Fields can appear or disappear based on other field values using the visibleWhen prop. This prop receives the reactive engine and can read any field's current state to determine visibility.
Stale Response Protection
The beginAsync / isLatest pattern prevents race conditions. If the user changes the country while states are loading, the system automatically discards the stale response.
Next Steps
Continue learning
Now that you understand the basics, explore more advanced concepts:
- Reactions: Deep dive into reaction lifecycle, conditions, and patterns
- Dynamic Forms: Learn all the ways to build adaptive forms
- Patterns: Best practices for structuring complex forms
- API Reference: Complete reference for all form system APIs