Frontend
Building a React frontend against a backend usually means writing the same plumbing over and over: a
fetch call here, a hand-written request type there, validation rules copied from the server, and a
pile of state to track whether the request is in flight, succeeded, or failed. Every time the backend
changes, you go hunting for the frontend code that drifted out of sync.
Arc removes that whole layer. When you build a command or query in C#, Arc generates a typed
TypeScript proxy for it. On the frontend you import that proxy and call its .use() hook — the
request shape, the response shape, and the validation rules all come along, type-checked end to end.
Change the C# and the generated proxy changes with it, so the compiler catches the drift instead of
your users.
How the pieces fit
Section titled “How the pieces fit”You write the backend once. The proxy generator runs on build and emits a typed client. Your React
code consumes it through each proxy’s static .use() hook — and because the types flow across the
boundary, there is no DTO to keep in sync and no untyped JSON to second-guess.
Run a command from React
Section titled “Run a command from React”A command is an intent to change something — open an account, check out a book. You define it in
C#; Arc generates a proxy whose static .use() hook gives you a reactive instance, change tracking,
and execution.
// Backend — the command and the read model it serves live together[Command]public record OpenAccount(AccountId Id, AccountHolder Owner){ public Task Handle(IMongoCollection<Account> accounts) => accounts.InsertOneAsync(new Account(Id, Owner));}// Frontend — the generated proxy, driven by its static .use() hookimport { OpenAccount } from './api/accounts/OpenAccount';
export const OpenAccountForm = () => { const [command] = OpenAccount.use();
const submit = async () => { const result = await command.execute(); if (result.isSuccess) { /* navigate away, show a toast, … */ } };
return ( <> <input value={command.owner} onChange={(e) => command.owner = e.target.value} /> <button onClick={submit} disabled={!command.hasChanges}>Open account</button> </> );};The .use() hook returns a tuple — the reactive command instance, then a setter for updating several
properties at once. Set a property and the component re-renders; call command.execute() and check
result.isSuccess. The validation you declare on the command is surfaced on the proxy, so invalid
input is caught on the client before the request ever leaves the browser.
Show live data with an observable query
Section titled “Show live data with an observable query”A query reads data. An observable query keeps reading — its .use() hook holds a live connection and
re-renders your component whenever the underlying read model changes, so your UI stays current without
polling.
import { AllAccounts } from './api/accounts/AllAccounts';
export const AccountsList = () => { const [accounts] = AllAccounts.use(); // observable: re-renders as the read model changes
return ( <ul> {accounts.data.map((account) => ( <li key={String(account.id)}>{account.owner}</li> ))} </ul> );};The result is a QueryResultWithState — accounts.data holds the rows, with isPerforming, hasData,
and validation state alongside. When a command writes and the AllAccounts read
model changes, every browser observing this query re-renders with the new list — no refresh, no refetch. That
live loop, from a C# command to a React list, is the payoff of building the whole stack on Arc.
Two ways to build: hooks or MVVM
Section titled “Two ways to build: hooks or MVVM”Most screens are simplest with the hooks shown above — they keep state in the component and read naturally. For complex, stateful screens you can opt into a Model-View-ViewModel structure that moves logic into a view model class.
| Reach for… | When |
|---|---|
| Hooks | The default. Forms, lists, and most screens — less boilerplate, state stays in the component. |
| MVVM for React | Complex screens with substantial view logic you want to test and reuse apart from the markup. |
Start with hooks; move a screen to MVVM only when its logic outgrows the component.