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.
Overview
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
Binding to Command Properties
Instead of binding your frontend components to read models from queries, you can bind directly to command properties. This provides several benefits:
Benefits
- Automatic Validation: Validation rules running on the frontend execute automatically as values change
- Change Tracking: The command tracks whether properties have changed from their original values
- Consistent State: Single source of truth for form data
- Type Safety: TypeScript types generated from backend command definitions
Example
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>
);
};
Initial Values
Commands need initial values to enable change tracking. The hasChanges property compares current values against these initial values.
Setting 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]);
When to Use Each Approach
- 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)
Change Tracking
The hasChanges property automatically tracks whether any command property differs from its initial value.
Basic Change Tracking
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
Using hasChanges in UI
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>
);
};
Loading Data from Queries
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>
);
};
Tracking Changes Across Multiple Commands
When you have a component with sub-components that each work with different commands, you can track the aggregate hasChanges state using Command Scope.
Example Without 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;
Example With Scope
import { CommandScope } from '@cratis/applications-react';
export const UserEditor = () => {
return (
<CommandScope>
{(scope) => (
<>
<ProfileForm />
<SettingsForm />
<button disabled={!scope.hasChanges}>
Save All Changes
</button>
</>
)}
</CommandScope>
);
};
See Command Scope for complete documentation.
Resetting to Initial Values
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);
};
Partial Updates
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'
});
Best Practices
- Always Provide Initial Values: Set initial values to enable proper change tracking
- Use Queries as Source: Load data from queries to initialize commands
- Check hasChanges: Prevent unnecessary API calls by checking if data actually changed
- Use CommandScope: For forms with multiple commands, use CommandScope to track aggregate state
- Reset After Save: Consider resetting initial values after successful save to clear
hasChanges - Type Safety: Leverage TypeScript to ensure initial values match command structure
Example: Complete Edit Form
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>
);
};