Argument Metadata
Generate form field definitions with types, labels, paths, and default values
MetadataDisplayReactor extends CandidDisplayReactor with visitor-based metadata generation for both form inputs and result display. It provides structured field definitions that are framework-agnostic and can be used with any UI library.
This class is ideal for building dynamic form UIs and result viewers for canisters discovered at runtime:
Argument Metadata
Generate form field definitions with types, labels, paths, and default values
Result Metadata
Generate display field definitions with type info and display transformations
Framework Agnostic
Works with TanStack Form, React Hook Form, or any UI library
Display Transformations
All DisplayReactor transformations (bigint → string, Principal → string)
import { MetadataDisplayReactor } from "@ic-reactor/candid"
const reactor = new MetadataDisplayReactor({ name: "my-dynamic-actor", canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", clientManager,})
await reactor.initialize()
// Get form field metadataconst argMeta = reactor.getArgumentMeta("icrc1_transfer")// argMeta.fields = [{ type: "record", label: "__arg0", fields: [...], defaultValues: {...} }]
// Get result display metadataconst resultMeta = reactor.getResultMeta("icrc1_transfer")// resultMeta.resultFields[0] = { type: "variant", displayType: "result", isResultType: true, ... }
// Call with transformed results wrapped in metadataconst result = await reactor.callMethod({ functionName: "icrc1_transfer", args: [{ to: { owner: "aaaaa-aa" }, amount: "1000000" }],})
/** * result = { * functionName: "icrc1_transfer", * results: [ * { * field: { type: "variant", displayType: "result", ... }, * value: { option: "Ok", value: { ... } }, * raw: { Ok: 1000000n } * } * ], * ... * } */import { MetadataDisplayReactor } from "@ic-reactor/candid"new MetadataDisplayReactor(config: CandidDisplayReactorParameters)| Parameter | Type | Required | Description |
|---|---|---|---|
clientManager | ClientManager | Yes | Client manager from @ic-reactor/core |
name | string | Yes | Required for logging & environment lookup |
canisterId | CanisterId | No | Optional if defined in environment |
candid | string | No | Candid service definition (avoids network fetch) |
The reactor uses the visitor pattern to generate metadata from Candid types:
┌─────────────────────────────────────────────────────────────────────────────┐│ MetadataDisplayReactor │├─────────────────────────────────────────────────────────────────────────────┤│ ││ INITIALIZATION (once, from raw IDL types): ││ ││ IDL.Service ──► ArgumentFieldVisitor ──► ServiceArgumentsMeta ││ { methodName: { ││ fields: ArgumentField[], ││ defaultValues: [...] ││ }} ││ ││ IDL.Service ──► ResultFieldVisitor ──► ServiceResultMeta ││ { methodName: { ││ resultFields: ResultField[], ││ returnCount: number ││ }} ││ │├─────────────────────────────────────────────────────────────────────────────┤│ ││ RUNTIME (per method call): ││ ││ INPUT: Display Values ──► encode ──► Candid Values ──► Canister ││ OUTPUT: Canister ──► Candid Values ──► decode ──► Display Values ││ ││ RENDERING: ResultField (metadata) + Display Value ──► UI Component ││ │└─────────────────────────────────────────────────────────────────────────────┘Get form field metadata for a method’s arguments.
const meta = reactor.getArgumentMeta("icrc1_transfer")MethodArgumentsMetainterface MethodArgumentsMeta { functionType: "query" | "update" functionName: string fields: ArgumentField[] // Form field definitions defaultValues: unknown[] // Initial form values}Each field includes type, label, and path for form binding:
| Field Type | Properties |
|---|---|
record | fields: ArgumentField[], defaultValues: {...} |
variant | fields: ArgumentField[], options: string[] |
tuple | fields: ArgumentField[], defaultValues: [...] |
optional | innerField: ArgumentField, defaultValue: null |
vector | itemField: ArgumentField, defaultValue: [] |
blob | itemField: ArgumentField, defaultValue: "" |
recursive | extract: () => ArgumentField |
principal | defaultValue: "", minLength, maxLength |
number | defaultValue: "", candidType: "nat" | "int" | ... |
text | defaultValue: "" |
boolean | defaultValue: false |
null | defaultValue: null |
const meta = reactor.getArgumentMeta("icrc1_transfer")
// meta.fields[0] is a record with nested fieldsconst transferArg = meta.fields[0] as RecordArgumentFieldconsole.log(transferArg.fields.map((f) => f.label))// ["to", "amount", "fee", "memo", "created_at_time"]
// Use defaultValues to initialize your formconst initialFormState = meta.defaultValues[0]// { to: { owner: "", subaccount: null }, amount: "", fee: null, ... }Get display metadata for a method’s return values.
const meta = reactor.getResultMeta("icrc1_transfer")MethodResultMetainterface MethodResultMeta { functionType: "query" | "update" functionName: string resultFields: ResultField[] // Display field definitions returnCount: number}The displayType property tells you what the value will be after transformation:
| Candid Type | displayType | Notes |
|---|---|---|
nat, int | "string" | BigInt → string |
nat64, int64 | "string" | BigInt → string |
nat8 - nat32 | "number" | Stays as JS number |
int8 - int32 | "number" | Stays as JS number |
float32, float64 | "number" | Stays as JS number |
principal | "string" | Principal.toText() |
text | "string" | No transformation |
bool | "boolean" | No transformation |
null | "null" | No transformation |
opt T | "nullable" | [value] → value, [] → null |
vec nat8 (blob) | "string" | → hex string |
vec T | "array" | Transformed array |
record {...} | "object" | Transformed object |
tuple (...) | "array" | Transformed array |
variant { Ok, Err } | "result" | Result type detected |
variant {...} | "variant" | Regular variant |
The visitor automatically detects Result variants (Ok/Err pattern):
const meta = reactor.getResultMeta("icrc1_transfer")const resultField = meta.resultFields[0] as VariantResultField
if (resultField.displayType === "result") { console.log("This is a Result type!")
// Access Ok and Err field definitions const okField = resultField.optionFields.find((f) => f.label === "Ok") const errField = resultField.optionFields.find((f) => f.label === "Err")}The visitor detects special formats from field labels:
Number Formats:
| Label Pattern | Format |
|---|---|
*timestamp*, *created_at*, *date* | timestamp |
*cycle*, *cycles* | cycle |
| (default) | normal |
Text Formats:
| Label Pattern | Format |
|---|---|
*email* | email |
*url*, *link* | url |
*phone* | phone |
*uuid* | uuid |
*btc*, *bitcoin* | btc |
*eth*, *ethereum* | eth |
*account_id* | account-id |
*canister*, *principal* | principal |
Get metadata for all methods at once:
const allArgMeta = reactor.getAllArgumentMeta()
// Access any method's metadataconst transferMeta = allArgMeta["icrc1_transfer"]const balanceMeta = allArgMeta["icrc1_balance_of"]Get result metadata for all methods:
const allResultMeta = reactor.getAllResultMeta()
// Access any method's result metadataconst transferResultMeta = allResultMeta["icrc1_transfer"]All methods from CandidDisplayReactor are available:
await reactor.registerMethod({ functionName: "custom_method", candid: "(text, nat) -> (variant { Ok : nat; Err : text })",})
// Metadata is generated and display codecs are initializedconst meta = reactor.getArgumentMeta("custom_method")const resultMeta = reactor.getResultMeta("custom_method")
// Now all Reactor methods work with display types + metadata wrappingconst result = await reactor.callMethod({ functionName: "custom_method", args: ["hello", "100"],})Call a method (registering it if needed) and get both the top-level metadata and the wrapped result in one step.
const { result, meta } = await reactor.callDynamicWithMeta({ functionName: "icrc1_fee", candid: "() -> (nat) query",})
// result = { functionName: "icrc1_fee", results: [...] }// meta = { resultFields: [...], returnCount: 1 }Perform a dynamic query call. Returns ResolvedMethodResult.
const result = await reactor.queryDynamic({ functionName: "icrc1_balance_of", candid: "(record { owner : principal }) -> (nat) query", args: [{ owner: "aaaaa-aa" }],})Perform a dynamic query call with TanStack Query caching. Returns ResolvedMethodResult.
const result = await reactor.fetchQueryDynamic({ functionName: "icrc1_balance_of", candid: "(record { owner : principal }) -> (nat) query", args: [{ owner: "aaaaa-aa" }],})import { MetadataDisplayReactor } from "@ic-reactor/candid"import type { RecordArgumentField, NumberArgumentField, OptionalArgumentField,} from "@ic-reactor/candid"
const reactor = new MetadataDisplayReactor({ name: "transfer-form", canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", candid: ICRC1_CANDID, clientManager,})
await reactor.initialize()
// Get transfer form metadataconst meta = reactor.getArgumentMeta("icrc1_transfer")
function buildForm() { const transferArg = meta.fields[0] as RecordArgumentField
return transferArg.fields.map((field) => { switch (field.type) { case "number": return { type: "input", inputType: "text", label: field.label, placeholder: `Enter ${(field as NumberArgumentField).candidType}`, defaultValue: "", } case "optional": return { type: "optional-input", label: field.label, innerType: (field as OptionalArgumentField).innerField.type, defaultValue: null, } case "record": return { type: "nested-form", label: field.label, fields: (field as RecordArgumentField).fields, } // ... handle other types } })}const meta = reactor.getResultMeta("icrc1_transfer")const result = await reactor.callMethod({ functionName: "icrc1_transfer", args: [transferArgs],})
function renderResult(field: ResultField, value: unknown) { switch (field.type) { case "number": const numField = field as NumberResultField if (numField.numberFormat === "timestamp") { return new Date(Number(value) / 1_000_000).toLocaleString() } return value // Already a string for nat/int
case "variant": const variantField = field as VariantResultField if (variantField.isResultType) { // Result type - value is already unwrapped by DisplayReactor return renderSuccess(value) } return renderVariant(value, variantField)
case "record": return renderRecord(value, field as RecordResultField)
default: return String(value) }}import { useForm } from "@tanstack/react-form"import { MetadataDisplayReactor } from "@ic-reactor/candid"import type { RecordArgumentField } from "@ic-reactor/candid"
function TransferForm({ reactor }: { reactor: MetadataDisplayReactor }) { const meta = reactor.getArgumentMeta("icrc1_transfer") const transferArg = meta.fields[0] as RecordArgumentField
const form = useForm({ defaultValues: meta.defaultValues[0], onSubmit: async ({ value }) => { const result = await reactor.callMethod({ functionName: "icrc1_transfer" as any, args: [value], }) console.log("Transfer result:", result) }, })
return ( <form onSubmit={(e) => { e.preventDefault() form.handleSubmit() }} > {transferArg.fields.map((field) => ( <DynamicField key={field.label} field={field} form={form} /> ))} <button type="submit">Transfer</button> </form> )}
function DynamicField({ field, form }) { switch (field.type) { case "number": return ( <form.Field name={field.label}> {(fieldApi) => ( <input type="text" value={fieldApi.state.value} onChange={(e) => fieldApi.handleChange(e.target.value)} placeholder={field.candidType} /> )} </form.Field> ) // ... other types }}| Use Case | Recommended Class |
|---|---|
| Dynamic form generation | MetadataDisplayReactor |
| Dynamic result rendering with types | MetadataDisplayReactor |
| Dynamic canister, display types only | CandidDisplayReactor |
| Dynamic canister, raw types | CandidReactor |
| Known canister, display types | DisplayReactor |
| Known canister, raw types | Reactor |