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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Initial Values”Commands need initial values to enable change tracking. The hasChanges property compares current values against these initial values.
Setting Initial Values
Section titled “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
Section titled “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
Section titled “Change Tracking”The hasChanges property automatically tracks whether any command property differs from its initial value.
Basic Change Tracking
Section titled “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 originalconsole.log(command.hasChanges); // falseUsing hasChanges in UI
Section titled “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
Section titled “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
Section titled “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
Section titled “Example Without Scope”// Each command tracks its own changesconst [profileCommand] = UpdateProfile.use(profile);const [settingsCommand] = UpdateSettings.use(settings);
// Need to manually check bothconst hasAnyChanges = profileCommand.hasChanges || settingsCommand.hasChanges;Example With Scope
Section titled “Example With Scope”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.
Resetting to Initial Values
Section titled “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 dataconst handleRefresh = async () => { const freshProfile = await fetchUserProfile(userId); setCommandValues(freshProfile);};Partial Updates
Section titled “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 emailsetCommandValues({ ...command, email: 'newemail@example.com'});Best Practices
Section titled “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
Section titled “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> );};