Skip to content

Async Data Loading

Load data asynchronously for use in form fields.

function OrderForm() {
const [products, setProducts] = useState<Array<{ id: string; name: string }>>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadProducts = async () => {
try {
const data = await fetch('/api/products').then((r) => r.json());
setProducts(data);
} finally {
setLoading(false);
}
};
loadProducts();
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<CommandForm command={CreateOrder}>
<SelectField<CreateOrder>
value={(c) => c.productId}
title='Product'
options={products}
optionIdField='id'
optionLabelField='name'
placeholder='Select a product...'
required
/>
<NumberField<CreateOrder>
value={(c) => c.quantity}
title='Quantity'
min={1}
required
/>
</CommandForm>
);
}

Load options based on another field’s value:

function LocationForm() {
const command = useCommandInstance(SaveLocation);
const [cities, setCities] = useState<Array<{ id: string; name: string }>>([]);
useEffect(() => {
if (command.country) {
// Load cities for selected country
const loadCities = async () => {
const data = await fetch(`/api/cities?country=${command.country}`).then(
(r) => r.json(),
);
setCities(data);
};
loadCities();
} else {
setCities([]);
}
}, [command.country]);
return (
<CommandForm command={SaveLocation}>
<SelectField<SaveLocation>
value={(c) => c.country}
title='Country'
options={countries}
optionIdField='id'
optionLabelField='name'
required
/>
<SelectField<SaveLocation>
value={(c) => c.city}
title='City'
options={cities}
optionIdField='id'
optionLabelField='name'
placeholder={
command.country ? 'Select a city...' : 'Select country first'
}
required
/>
</CommandForm>
);
}

The examples above load data for a field’s options - the form’s own values still come from initialValues/currentValues, fetched by hand. CommandForm can fetch its initial values itself instead, from a single-instance query:

import { CommandForm, InputTextField } from '@cratis/arc.react/commands';
import { GetUserProfile } from './queries';
import { UpdateProfile } from './commands';
function ProfileEditor({ userId }: { userId: string }) {
return (
<CommandForm
command={UpdateProfile}
populateFromQuery={GetUserProfile}
populateFromQueryArgs={{ userId }}
>
<InputTextField<UpdateProfile>
value={(c) => c.firstName}
title='First name'
/>
<InputTextField<UpdateProfile> value={(c) => c.lastName} title='Last name' />
<InputTextField<UpdateProfile> value={(c) => c.email} title='Email' />
</CommandForm>
);
}

Each field is matched onto the query’s result by property name - the command’s firstName is seeded from the query result’s firstName, and so on. GetUserProfile must be a single-instance query - a query returning multiple instances throws. For an observable query, use populateFromObservableQuery instead; both read their arguments from populateFromQueryArgs.

The resolved data becomes the form’s baseline, not just a one-time overlay - command.hasChanges reads false right after the query resolves, and only an edit the user actually makes flips it to true. Validation reruns only when the resolved data itself changes, not on every unrelated re-render - so a query that happens to return the same value twice does not re-trigger validation.

Two field props refine what a query populates:

PropEffect
noInitialValueSkips this field entirely, even if the query result has a same-named property.
initialValueOverrides how the field’s value is derived from the query result - either a property accessor matched by name, or an arbitrary function that composes a value from the whole result.
populationKeyIdentifies semantics captured by initialValue, such as a locale. Change it to recompute the field from the current query result with the latest callback.
<InputTextField<UpdateProfile>
value={(c) => c.displayName}
initialValue={(profile: UserProfile) => `${profile.firstName} ${profile.lastName}`}
title='Display name'
/>

An inline callback may close over values that CommandForm cannot discover. Give that semantic input a populationKey; when the key changes, CommandForm evaluates the latest callback once against the current population source and stores the result as the new baseline:

function DisplayNameField({ locale }: { locale: string }) {
return (
<InputTextField<UpdateProfile>
value={(c) => c.displayName}
initialValue={(profile: UserProfile) =>
locale === 'nb'
? `${profile.lastName}, ${profile.firstName}`
: `${profile.firstName} ${profile.lastName}`
}
populationKey={locale}
title='Display name'
/>
);
}

CommandForm keeps each mounted field registration stable and retains the latest committed accessor and initialValue callback. Recreating an equivalent inline function does not repopulate the field, and an interrupted concurrent render cannot publish a callback that never committed. When a callback and its population source or populationKey change in the same commit, CommandForm evaluates the new callback once; it never applies the previous callback for one render. The callback runs again only when the population source changes, automatic field metadata changes, or its populationKey changes. Use a stable semantic value such as a locale, selected mode, or revision - not a newly created object on every render.