Skip to content

Execute a command from React

Goal: a user clicks a button or fills a form, and a command runs — with validation feedback, a disabled-while-running button, and no hand-written API client.

After a configured Debug proxy-generation build, Arc supplies typed command proxies. This recipe assumes the standalone backend and full-stack tutorial setup, which supplies the Vite bootstrap, Components prerequisites, providers, and dialog composition. For an inline form in an existing Vite app, frontend getting started uses Arc’s CommandForm with Components fields, and shows the route without Components. You don’t write a fetch, redeclare the command’s shape, or wire validation — the proxy already knows the types, and CommandDialog drives the whole interaction.

  1. Use CommandDialog for a form. Pass the command constructor; the dialog instantiates it, renders the confirm/cancel buttons, and disables confirm while it executes. Use command form fields for the inputs:

    AddAuthor.tsx
    import { useState } from 'react';
    import { Guid } from '@cratis/fundamentals';
    import { CommandDialog } from '@cratis/components/CommandDialog';
    import { InputTextField } from '@cratis/components/CommandForm';
    import { RegisterAuthor } from './Authors/RegisterAuthor'; // generated proxy
    export const AddAuthor = () => {
    const [id] = useState(() => Guid.create());
    return (
    <CommandDialog<RegisterAuthor> command={RegisterAuthor} title="Add author" okLabel="Add" initialValues={{ id }}>
    <InputTextField<RegisterAuthor> value={i => i.name} title="Name" />
    </CommandDialog>
    );
    };

    Mount a new dialog instance per operation and unmount on close. id is stable during editing and fresh for the next registration; a required Guid is not automatically filled by the proxy.

    The field accessor i => i.name is a property on the generated type — rename it in C#, rebuild, and the frontend stops compiling until you fix it.

  2. Inject context the form needs to be valid via initialValues. A parent id the user shouldn’t type goes here — not in onBeforeExecute, which fires too late to affect validity:

    This form fragment assumes the book dialog’s imports and per-operation bookId state. Keep authorId as the generated Guid, not a string.

    <CommandDialog<AddBook> command={AddBook} title="Add book" initialValues={{ authorId, bookId }}>
    <InputTextField<AddBook> value={i => i.title} title="Title" />
    </CommandDialog>
  3. Validation surfaces itself. Supported rules extracted from the command and its value types run client-side; the server remains authoritative, including async/custom/state-dependent rules that cannot be extracted, and CommandDialog renders the messages against the fields. You write none of that wiring.

For executing a command without a dialog (a plain button, custom flow), the proxy exposes an execute call that returns a CommandResult — check isSuccess and read any returned value off it.