Component Registry
How to register your React components with Rilaykit.
Rilaykit is headless: you provide the components, Rilaykit provides the logic. The registry — a ril catalog — maps a type string to your renderer.
Registering a Component
Use .component(type, entry) on a ril instance. Each call returns a new instance (immutable, chainable). Registering an existing type throws DuplicateError unless the entry sets replace: true.
import { ril } from 'rilaykit';
import { TextInput } from '@/components/TextInput';
import { Select } from '@/components/Select';
export const r = ril
.create()
.component('text', {
name: 'Text Input',
renderer: TextInput,
})
.component('select', {
name: 'Select',
description: 'A dropdown for selecting options.',
renderer: Select,
defaultProps: { options: [] },
});Entry options
| Option | Description |
|---|---|
renderer | (ctx: ComponentRenderContext<TProps>) => React.ReactElement |
name | Human-readable name |
description | Purpose of the component (also surfaced to agent manifests) |
defaultProps | Merged under each field's props (field props win) |
propsSchema | Standard Schema validating props — must validate synchronously |
meta | Arbitrary metadata, passed through to the render context |
replace | Allow overwriting an already-registered type |
The same catalog also registers .tool() and .part() entries for agent UIs, and .renderers({ components: {...} }) attaches renderers to schema-only entries later — useful when the catalog is built server-side and renderers attach on the client.
The ComponentRenderContext
Your renderer receives a single context object. Form state lives under field (absent for static, non-field components):
interface ComponentRenderContext<TProps = Record<string, unknown>> {
id: string; // unique field id
props: TProps; // your custom props (defaultProps merged in)
field?: FieldBinding; // form binding, when rendered as a field
conditions?: FieldConditions; // required/disabled/readonly resolution
children?: React.ReactNode;
meta?: Record<string, unknown>;
}
interface FieldBinding {
value: unknown;
onChange: (value: unknown) => void;
onBlur: () => void; // feeds validation timing (mode/reValidateMode)
error?: FieldError[];
disabled?: boolean;
isValidating?: boolean;
touched?: boolean;
}Example: a TextInput renderer
import type { ComponentRenderContext } from 'rilaykit';
interface TextInputProps {
label: string;
placeholder?: string;
required?: boolean;
}
export function TextInput({ id, props, field }: ComponentRenderContext<TextInputProps>) {
const showError = field?.touched && field.error?.length;
return (
<div className="form-control">
<label htmlFor={id}>
{props.label}
{props.required && <span>*</span>}
</label>
<input
id={id}
type="text"
value={(field?.value as string) ?? ''}
onChange={(e) => field?.onChange(e.target.value)}
onBlur={() => field?.onBlur()}
placeholder={props.placeholder}
disabled={field?.disabled}
aria-invalid={!!showError}
/>
{showError ? (
<p role="alert" className="error-message">
{field.error?.[0]?.message}
</p>
) : null}
</div>
);
}Gating on field.touched pairs with the default validation timing (mode: 'onTouched'): errors appear after first blur — or on submit, which marks errored fields touched — then clear live.