Loading...
Loading...
Best practices and patterns for RilayKit — a headless, type-safe React framework for building dynamic forms and multi-step workflows with builder pattern APIs, Standard Schema validation, and granular Zustand-powered hooks. Use this skill when working on projects that import @rilaykit/core, @rilaykit/forms, or @rilaykit/workflow packages, or when building forms, multi-step flows, component registries, or conditional field logic with RilayKit.
npx skill4agent add andyoucreate/rilaykit rilaykit@rilaykit/core → Foundation: ril instance, component registry, validation, conditions
@rilaykit/forms → Form builder, components, hooks (depends on core)
@rilaykit/workflow → Flow builder, navigation, persistence, analytics (depends on core + forms)import { ril } from "@rilaykit/core";
export const r = ril
.create()
.addComponent("input", {
name: "Text Input",
renderer: InputRenderer,
defaultProps: { placeholder: "Enter..." },
})
.addComponent("select", {
name: "Select",
renderer: SelectRenderer,
validation: { validate: z.string().optional() },
})
.configure({
rowRenderer: RowRenderer,
bodyRenderer: BodyRenderer,
fieldRenderer: FieldRenderer,
submitButtonRenderer: SubmitButtonRenderer,
stepperRenderer: StepperRenderer,
nextButtonRenderer: NextButtonRenderer,
previousButtonRenderer: PreviousButtonRenderer,
});import type { ComponentRenderProps } from "@rilaykit/core";
type ComponentRenderer<T = any> = (props: ComponentRenderProps<T>) => React.ReactElement;
// Props received by every renderer:
// id, value, onChange, onBlur, props, error, disabled, contextconst loginForm = r
.form("login")
.add(
{ id: "email", type: "input", props: { label: "Email" }, validation: { validate: [required(), email()] } },
{ id: "password", type: "input", props: { type: "password" }, validation: { validate: [required()] } },
);.add().add()import { Form, FormBody, FormSubmitButton } from "@rilaykit/forms";
<Form formConfig={loginForm} onSubmit={handleSubmit} defaultValues={{ email: "" }}>
<FormBody />
<FormSubmitButton>Sign In</FormSubmitButton>
</Form>const onboarding = r
.flow("onboarding", "User Onboarding")
.addStep({ id: "personal", title: "Personal Info", formConfig: personalForm })
.addStep({
id: "company",
title: "Company",
formConfig: companyForm,
conditions: { visible: when("personal.userType").equals("business") },
onAfterValidation: async (stepData, helper) => {
const result = await fetchCompany(stepData.siren);
helper.setNextStepFields({ company: result.name });
},
})
.configure({ analytics: myAnalytics })
.build();import { Workflow, WorkflowStepper, WorkflowBody, WorkflowNextButton, WorkflowPreviousButton } from "@rilaykit/workflow";
<Workflow workflowConfig={onboarding} onWorkflowComplete={handleComplete} defaultValues={defaults}>
<WorkflowStepper />
<WorkflowBody />
<div className="flex justify-between">
<WorkflowPreviousButton />
<WorkflowNextButton>{(p) => p.isLastStep ? "Complete" : "Next"}</WorkflowNextButton>
</div>
</Workflow>import { required, email, pattern, custom, async as asyncValidator } from "@rilaykit/core";
import { z } from "zod";
validation: {
validate: [
required("Required"), // RilayKit built-in
z.string().email("Invalid"), // Zod
asyncValidator(checkEmail, "Already exists"), // Async
],
validateOnBlur: true,
debounceMs: 200,
}import { when } from "@rilaykit/core";
// Field-level conditions
conditions: {
visible: when("accountType").equals("business"),
required: when("accountType").equals("business"),
disabled: when("status").equals("locked"),
}
// Combine with and/or
when("type").equals("premium")
.and(when("status").in(["active", "verified"]))
.or(when("age").greaterThan(65))
// Operators: equals, notEquals, greaterThan, lessThan, contains, notContains,
// matches, in, notIn, exists, notExists// Field-level (only re-renders when that field changes)
const email = useFieldValue<string>("email");
const errors = useFieldErrors("email");
const { setValue } = useFieldActions("email");
// Form-level
const isSubmitting = useFormSubmitting();
const allValues = useFormValues();
const { reset } = useFormActions();// Define once, use across multiple flows
export const personalInfoStep = (t: TranslationFn): StepDefinition => ({
id: "personalInfo",
title: t("steps.personalInfo.title"),
formConfig: form.create(r).add(/* fields */),
});
// Conditionally add steps
if (!hasExistingClient) {
workflowFlow = workflowFlow.addStep(personalInfoStep(t));
}.add().addStep().configure()useFieldValueuseFormSubmittingdata.stepId.fieldId.build()<Workflow>