Table of Contents

Command Scope

If you want to track commands and create an aggregation of their status at a compositional level, the command scope provides a React context for this. This is typically useful when having something like a top level toolbar with a Save button that you want to enable or disable depending on whether or not there are changes within any components used within it, or a loading indicator when commands or queries are being performed.

Using the toolbar scenario as an example; at the top level we can wrap everything in the <CommandScope> component. This will establish a React context for this part of the hierarchy and track any commands and queries used within any descendants.

import { CommandScope } from '@cratis/arc/commands';

export const MyComposition = () => {
    const [hasChanges, setHasChanges] = useState(false);
    const [isPerforming, setIsPerforming] = useState(false);

    return (
        <CommandScope 
            setHasChanges={setHasChanges}
            setIsPerforming={setIsPerforming}>
            <Toolbar hasChanges={hasChanges} isPerforming={isPerforming}/>
            <FirstComponent/>
            <SecondComponent/>
        </CommandScope>
    );
};

Hierarchical Scopes

Command scopes can be nested to create a hierarchy. When you add a <CommandScope> inside another one, the inner scope automatically registers itself with the nearest outer scope. Commands always bind to the nearest enclosing scope, so each part of the component tree only tracks the commands directly beneath it. The outer scope aggregates state across all inner scopes, giving you both local and global views of changes, performing state, validation failures, and exceptions.

export const MyPage = () => {
    const [hasChanges, setHasChanges] = useState(false);
    const [hasValidationFailures, setHasValidationFailures] = useState(false);

    return (
        <CommandScope setHasChanges={setHasChanges}>
            {/* PageToolbar sees aggregate state for the whole page */}
            <PageToolbar hasChanges={hasChanges} hasValidationFailures={hasValidationFailures}/>
            <Section1>
                <CommandScope>
                    {/* SectionToolbar only sees state for Section1's commands */}
                    <SectionToolbar/>
                    <SectionContent/>
                </CommandScope>
            </Section1>
        </CommandScope>
    );
};

In this example:

  • Commands inside <Section1> bind to the inner <CommandScope>.
  • The outer <CommandScope> sees their state through the nested scope link.
  • pageScope.hasValidationFailures is true whenever any inner scope has failures.

Validation Failures and Exceptions

The CommandScope tracks which commands produced validation failures or exceptions after execution. These are cleared automatically when a command executes again — you never need to reset them manually.

Checking aggregate state

import { useCommandScope } from '@cratis/arc/commands';

export const Toolbar = () => {
    const scope = useCommandScope();

    return (
        <div>
            {scope.hasValidationFailures && (
                <span className="error">Some inputs have validation errors.</span>
            )}
            {scope.hasExceptions && (
                <span className="error">An unexpected error occurred.</span>
            )}
        </div>
    );
};

Inspecting per-command detail

When you need to know exactly which command had a problem and what the errors were, use the validationFailures and exceptions maps. Both are ReadonlyMap<ICommand, ...>.

If you only need a flattened aggregate view (across the current scope and all nested child scopes), use aggregatedValidationFailures and aggregatedExceptions.

const scope = useCommandScope();

// Validation failures — map of command → ValidationResult[]
for (const [command, failures] of scope.validationFailures) {
    failures.forEach(f => console.log(f.message, f.members));
}

// Exceptions — map of command → string[]
for (const [command, messages] of scope.exceptions) {
    messages.forEach(m => console.log(m));
}

// Flattened aggregates across this scope and child scopes
scope.aggregatedValidationFailures.forEach(f => console.log(f.message));
scope.aggregatedExceptions.forEach(m => console.log(m));

Only the own commands of a scope appear in its validationFailures and exceptions maps. Commands from nested child scopes appear in those child scopes' maps, but bubble up to the parent via hasValidationFailures and hasExceptions.

Automatic clearing on re-execution

When a command executes again, its previous failures and exceptions are cleared before the new execution begins. This means a scope's hasValidationFailures and hasExceptions always reflect the result of the most recent execution, not a stale prior result.

// First execute — validation fails
await scope.execute();
console.log(scope.hasValidationFailures); // true

// User fixes the input; execute again — succeeds
await scope.execute();
console.log(scope.hasValidationFailures); // false — automatically cleared

You can provide callbacks to the <CommandScope> component to react to command execution results. These callbacks are invoked for any command tracked within the scope, and each receives the specific command that was executed together with its result.

Before Execution

The onBeforeExecute callback is called just before each command is executed:

<CommandScope onBeforeExecute={(command) => console.log('About to execute', command)}>
    {/* ... */}
</CommandScope>

Result Callbacks

The result callbacks mirror the callbacks available on CommandResult but also include the command instance so you can identify which command produced the result:

<CommandScope
    onSuccess={(command, result) => {
        console.log('Command succeeded:', command, result);
    }}
    onFailed={(command, result) => {
        console.log('Command failed:', command, result);
    }}
    onException={(command, result) => {
        console.log('Command threw an exception:', command, result.exceptionMessages);
    }}
    onUnauthorized={(command, result) => {
        console.log('Command was unauthorized:', command);
    }}
    onValidationFailure={(command, result) => {
        console.log('Command had validation errors:', command, result.validationResults);
    }}>
    {/* ... */}
</CommandScope>
Callback When called
onBeforeExecute(command) Before each command is executed
onSuccess(command, result) When a command executes successfully
onFailed(command, result) When a command fails for any reason
onException(command, result) When a command fails due to an exception
onUnauthorized(command, result) When a command fails due to an authorization failure
onValidationFailure(command, result) When a command fails due to validation errors

Note that onFailed is always called alongside the more specific callbacks (onException, onUnauthorized, onValidationFailure) when the command fails.

Command Scope API

The command scope provides the following properties and methods:

Name Type Description
hasChanges Boolean Whether or not there are changes in any commands within the scope
isPerforming Boolean Whether or not any commands or queries are currently being performed
hasValidationFailures Boolean Whether any commands in this scope or child scopes have validation failures from the last execution
hasExceptions Boolean Whether any commands in this scope or child scopes produced exceptions in the last execution
validationFailures ReadonlyMap<ICommand, ValidationResult[]> Validation failures per command for this scope's own commands
exceptions ReadonlyMap<ICommand, string[]> Exception messages per command for this scope's own commands
aggregatedValidationFailures ReadonlyArray<ValidationResult> Flattened validation failures across this scope and all child scopes
aggregatedExceptions ReadonlyArray<string> Flattened exception messages across this scope and all child scopes
parent ICommandScope | undefined The parent scope, if this scope is nested
execute() Promise<CommandResults> Execute all commands with changes within the scope
revertChanges() void Revert any changes to commands within the scope
addCommand(command) void Manually add a command for tracking (usually done automatically)
addQuery(query) void Manually add a query for tracking (usually done automatically)
addChildScope(scope) void Register a child scope for aggregate state propagation (done automatically)

Using Command Scope in React

To consume the command scope context you can use the hook that is provided.

import { useCommandScope } from '@cratis/arc/commands';

export const Toolbar = ({ hasChanges, isPerforming }) => {
    const commandScope = useCommandScope();

    return (
        <div>
            <button 
                disabled={!hasChanges || isPerforming}
                onClick={() => commandScope.execute()}>
                Save
            </button>
            <button 
                disabled={!hasChanges || isPerforming}
                onClick={() => commandScope.revertChanges()}>
                Cancel
            </button>
            {isPerforming && <Spinner />}
        </div>
    );
};

The hook is a convenience hook that makes it easier to get the context. You can also consume the context directly by using its consumer:

import { CommandScopeContext } from '@cratis/arc/commands';

export const Toolbar = () => {
    return (
        <div>
            <CommandScopeContext.Consumer>
                {value => {
                    return (
                        <button disabled={!value.hasChanges}>Save</button>
                    )
                }}
            </CommandScopeContext.Consumer>
        </div>
    );
};

Using Command Scope in ViewModels

The command scope can also be injected into ViewModels through dependency injection. The ViewModel will automatically receive the closest command scope in the component hierarchy.

import { ICommandScope } from '@cratis/arc.react/commands';
import { injectable } from 'tsyringe';

@injectable()
export class MyViewModel {
    constructor(private readonly _commandScope: ICommandScope) {
    }

    get hasChanges(): boolean {
        return this._commandScope.hasChanges;
    }

    get isPerforming(): boolean {
        return this._commandScope.isPerforming;
    }

    async save() {
        if (!this.hasChanges || this.isPerforming) {
            return;
        }
        
        await this._commandScope.execute();
    }

    cancel() {
        this._commandScope.revertChanges();
    }
}

Automatic Command and Query Tracking

For the <FirstComponent> we could then have something like below:

export const FirstComponent = () => {
    const myCommand = MyCommand.use();

    return (
        <div>
            <input type="text" value={command.someValue} onChange={(e,v) => myCommand.someValue = v; }/>
        </div>
    )
}

Commands created with the use() hook are automatically added to the nearest command scope.

Queries are also automatically tracked:

export const SecondComponent = () => {
    const [result] = useQuery(MyQuery, { id: '123' });

    return (
        <div>
            {result.data && <DisplayData data={result.data} />}
        </div>
    )
}

Any changes to properties within commands will bubble up to the context and affect the state flags (hasChanges, isPerforming).