Skip to content

Data Binding and Initial Values

Commands act as the data model for your forms and UI components. Understanding how to bind to command properties and manage initial values is essential for building responsive, change-tracked forms.

The command holds properties that represent the payload of what you want to have happen. These properties are:

  • Subject to validation rules
  • Subject to business rules
  • Often sourced from read models coming from queries
  • Used for operations that are typically updates to existing data

Instead of binding your frontend components to read models from queries, you can bind directly to command properties. This provides several benefits:

  1. Automatic Validation: Validation rules running on the frontend execute automatically as values change
  2. Change Tracking: The command tracks whether properties have changed from their original values
  3. Consistent State: Single source of truth for form data
  4. Type Safety: TypeScript types generated from backend command definitions
import { UpdateProfile } from './generated/commands';
export const ProfileEditor = () => {
const [command] = UpdateProfile.use({
userId: currentUser.id,
firstName: currentUser.firstName,
lastName: currentUser.lastName,
email: currentUser.email
});
return (
<form>
<input
type="text"
value={command.firstName}
onChange={(e) => command.firstName = e.target.value}
/>
<input
type="text"
value={command.lastName}
onChange={(e) => command.lastName = e.target.value}
/>
<input
type="email"
value={command.email}
onChange={(e) => command.email = e.target.value}
/>
</form>
);
};

Commands need initial values to enable change tracking. The hasChanges property compares current values against these initial values.

There are two recommended ways to set initial values:

1. Via React Hook (Recommended):

const [command] = UpdateProfile.use({
userId: 'user-123',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});

2. Via setCommandValues Function:

const [command, setCommandValues] = UpdateProfile.use();
useEffect(() => {
// Load data from API or query
fetchUserProfile(userId).then(profile => {
setCommandValues(profile);
});
}, [userId]);

3. Via setInitialValues (Advanced):

const [command] = UpdateProfile.use();
useEffect(() => {
fetchUserProfile(userId).then(profile => {
command.setInitialValues(profile);
});
}, [userId]);
  • React Hook Parameter: When you have initial data available at component mount
  • setCommandValues: When loading data asynchronously or from queries
  • setInitialValues: Advanced scenarios requiring direct control (rare)

The hasChanges property automatically tracks whether any command property differs from its initial value.

const [command] = UpdateProfile.use({
firstName: 'John',
lastName: 'Doe'
});
console.log(command.hasChanges); // false
command.firstName = 'Jane';
console.log(command.hasChanges); // true
command.firstName = 'John'; // Reverted to original
console.log(command.hasChanges); // false
export const ProfileEditor = () => {
const [command] = UpdateProfile.use(currentProfile);
const handleSave = async () => {
if (command.hasChanges) {
await command.execute();
}
};
return (
<form>
{/* Form fields */}
<button
onClick={handleSave}
disabled={!command.hasChanges}
>
Save Changes
</button>
{command.hasChanges && (
<div className="warning">
You have unsaved changes
</div>
)}
</form>
);
};

A common pattern is loading data from a query and using it to initialize a command:

import { GetUserProfile } from './generated/queries';
import { UpdateProfile } from './generated/commands';
export const ProfileEditor = ({ userId }: { userId: string }) => {
const profile = GetUserProfile.use({ userId });
const [command, setCommandValues] = UpdateProfile.use();
useEffect(() => {
if (profile) {
setCommandValues({
userId: profile.userId,
firstName: profile.firstName,
lastName: profile.lastName,
email: profile.email
});
}
}, [profile]);
if (!profile) {
return <div>Loading...</div>;
}
return (
<form>
<input
value={command.firstName}
onChange={(e) => command.firstName = e.target.value}
/>
{/* More fields... */}
<button disabled={!command.hasChanges}>
Save
</button>
</form>
);
};

When you have a component with sub-components that each work with different commands, you can track the aggregate hasChanges state using Command Scope.

// Each command tracks its own changes
const [profileCommand] = UpdateProfile.use(profile);
const [settingsCommand] = UpdateSettings.use(settings);
// Need to manually check both
const hasAnyChanges = profileCommand.hasChanges || settingsCommand.hasChanges;
import { CommandScope } from '@cratis/arc.react/commands';
export const UserEditor = () => {
return (
<CommandScope>
{(scope) => (
<>
<ProfileForm />
<SettingsForm />
<button disabled={!scope.hasChanges}>
Save All Changes
</button>
</>
)}
</CommandScope>
);
};

See Command Scope for complete documentation.

To reset a command to its initial state:

const [command, setCommandValues] = UpdateProfile.use(initialProfile);
const handleReset = () => {
setCommandValues(initialProfile);
};
// Or get fresh data
const handleRefresh = async () => {
const freshProfile = await fetchUserProfile(userId);
setCommandValues(freshProfile);
};

You can update only specific properties while keeping others unchanged:

const [command, setCommandValues] = UpdateProfile.use({
userId: 'user-123',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});
// Update only email
setCommandValues({
...command,
email: 'newemail@example.com'
});
  1. Always Provide Initial Values: Set initial values to enable proper change tracking
  2. Use Queries as Source: Load data from queries to initialize commands
  3. Check hasChanges: Prevent unnecessary API calls by checking if data actually changed
  4. Use CommandScope: For forms with multiple commands, use CommandScope to track aggregate state
  5. Reset After Save: Consider resetting initial values after successful save to clear hasChanges
  6. Type Safety: Leverage TypeScript to ensure initial values match command structure
import { GetUserProfile } from './generated/queries';
import { UpdateProfile } from './generated/commands';
import { useEffect, useState } from 'react';
export const UserProfileEditor = ({ userId }: { userId: string }) => {
const profile = GetUserProfile.use({ userId });
const [command, setCommandValues] = UpdateProfile.use();
const [saved, setSaved] = useState(false);
// Initialize command when profile loads
useEffect(() => {
if (profile) {
setCommandValues(profile);
}
}, [profile]);
const handleSave = async () => {
if (!command.hasChanges) return;
const result = await command.execute();
if (result.isSuccess) {
// Reset initial values to current values
setCommandValues(command);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
}
};
const handleReset = () => {
if (profile) {
setCommandValues(profile);
}
};
if (!profile) {
return <div>Loading...</div>;
}
return (
<div>
<form>
<div>
<label>First Name:</label>
<input
value={command.firstName}
onChange={(e) => command.firstName = e.target.value}
/>
</div>
<div>
<label>Last Name:</label>
<input
value={command.lastName}
onChange={(e) => command.lastName = e.target.value}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={command.email}
onChange={(e) => command.email = e.target.value}
/>
</div>
</form>
<div>
<button
onClick={handleSave}
disabled={!command.hasChanges}
>
Save
</button>
<button
onClick={handleReset}
disabled={!command.hasChanges}
>
Reset
</button>
</div>
{saved && <div className="success">Profile saved successfully!</div>}
{command.hasChanges && <div className="info">You have unsaved changes</div>}
</div>
);
};