Debugging
Tools and techniques for debugging RilayKit forms and workflows — monitoring adapters, state inspection, and common troubleshooting patterns.
This guide covers RilayKit's built-in debugging tools, reusable debug panel patterns, and fixes for common issues.
Development Monitoring
DevelopmentAdapter logs every monitoring event to the console with structured, grouped output: form events, validation results, workflow transitions, and periodic performance summaries.
import {
initializeMonitoring,
getGlobalMonitor,
DevelopmentAdapter,
} from '@rilaykit/core';
if (process.env.NODE_ENV === 'development') {
initializeMonitoring(
{ enabled: true, sampleRate: 1.0, flushInterval: 5000 },
{ environment: 'development' }
);
getGlobalMonitor()?.addAdapter(new DevelopmentAdapter());
}Monitoring is opt-in: without initializeMonitoring no events are collected and there is zero overhead. DevelopmentAdapter wraps a ConsoleAdapter at 'debug' level, so all severities are visible.
Form Debug Panel
Place this inside a <Form> to inspect values, errors, and submit state live. It reads from the Zustand store and renders nothing in production.
import { useFormValues, useFormSubmitState, useFormStore } from '@rilaykit/forms/react';
import { useStore } from 'zustand';
function FormDebugPanel() {
const values = useFormValues();
const store = useFormStore();
const errors = useStore(store, (state) => state.errors);
const submitState = useFormSubmitState();
if (process.env.NODE_ENV === 'production') return null;
return (
<details open>
<summary>Form Debug</summary>
<div style={{ fontFamily: 'monospace', fontSize: 12 }}>
<h4>Values</h4>
<pre>{JSON.stringify(values, null, 2)}</pre>
<h4>Errors</h4>
<pre>{JSON.stringify(errors, null, 2)}</pre>
<h4>Submit State</h4>
<pre>{JSON.stringify(submitState, null, 2)}</pre>
</div>
</details>
);
}<Form formConfig={myForm} onSubmit={handleSubmit}>
<FormField fieldId="email" />
<FormDebugPanel />
</Form>The errors map is path-keyed. Cross-field issues with no matching field land in the reserved __form__ bucket — read it with useFormErrors().
Workflow Debug Panel
The same pattern for workflow state — place it inside a <WorkflowProvider> (or <Flow>).
import {
useFlow,
useFlowStepIndex,
useVisitedSteps,
usePassedSteps,
useSkippedSteps,
useFlowNavigationState,
} from '@rilaykit/workflow/react';
function WorkflowDebugPanel() {
const { currentStep, context } = useFlow();
const currentStepIndex = useFlowStepIndex();
const visitedSteps = useVisitedSteps();
const passedSteps = usePassedSteps();
const skippedSteps = useSkippedSteps();
const navigationState = useFlowNavigationState();
if (process.env.NODE_ENV === 'production') return null;
return (
<details open>
<summary>Workflow Debug</summary>
<div style={{ fontFamily: 'monospace', fontSize: 12 }}>
<h4>Current Step</h4>
<pre>{JSON.stringify({ index: currentStepIndex, step: currentStep.id }, null, 2)}</pre>
<h4>Progress</h4>
<pre>{JSON.stringify({
totalSteps: context.totalSteps,
visitedSteps: [...visitedSteps],
passedSteps: [...passedSteps],
skippedSteps: [...skippedSteps],
}, null, 2)}</pre>
<h4>Navigation State</h4>
<pre>{JSON.stringify(navigationState, null, 2)}</pre>
</div>
</details>
);
}Inspecting Field State
useFieldState(fieldId) returns the complete store slice for one field.
import { useFieldState } from '@rilaykit/forms/react';
function FieldDebug({ fieldId }: { fieldId: string }) {
const state = useFieldState(fieldId);
if (process.env.NODE_ENV === 'production') return null;
return <pre style={{ fontSize: 10, opacity: 0.7 }}>{JSON.stringify(state, null, 2)}</pre>;
}interface FieldState {
value: unknown;
errors: FieldError[];
validationState: 'idle' | 'validating' | 'valid' | 'invalid';
touched: boolean;
dirty: boolean;
}Testing Conditions
Conditions built with when() expose a synchronous, side-effect-free evaluate() — test them outside React with plain data.
import { when } from '@rilaykit/core';
const condition = when('accountType').equals('business');
condition.evaluate({ accountType: 'business' }); // true
const compound = when('age').greaterThanOrEqual(18)
.and(when('country').in(['US', 'CA', 'UK']));
compound.evaluate({ age: 21, country: 'US' }); // true
compound.evaluate({ age: 16, country: 'US' }); // falsePerformance Profiling
Every RilayMonitor exposes a PerformanceProfiler via getProfiler() for high-resolution timing.
import { getGlobalMonitor } from '@rilaykit/core';
const profiler = getGlobalMonitor()?.getProfiler();
profiler?.start('form-render');
// ... render form
profiler?.end('form-render');
profiler?.mark('validation-start');
// ... run validation
profiler?.mark('validation-end');
profiler?.measure('full-validation', 'validation-start', 'validation-end');
console.log(profiler?.getAllMetrics());| Method | Description |
|---|---|
.start(label) | Starts a timer. |
.end(label) | Ends the timer, returns the recorded PerformanceMetrics. |
.mark(name) | Places a named timestamp via performance.mark. |
.measure(name, startMark, endMark?) | Measures the duration between two marks. |
.getMetrics(label) | Metrics for one label. |
.getAllMetrics() | All metrics as Record<string, PerformanceMetrics>. |
.clear(label?) | Clears one label, or everything. |
Common Issues and Solutions
Field not visible
- Check conditions. Test the field's visibility condition in isolation with
condition.evaluate(mockData). - Verify field IDs. The
idin.add({ id: '...' })must exactly match<FormField fieldId="..." />. - Inspect the Form Debug Panel to confirm the field the condition depends on holds the expected value.
Validation not running
- Check the validation format. It's
validation: { validate: [...] }— thevalidatekey is required. - Check the validation mode. The default
modeis'onTouched': a field first validates when it loses focus, then re-validates live (reValidateMode: 'onChange'). For validation on first keystroke, set.setValidation({ mode: 'onChange' })on the form. - Verify the field is touched. With
'onTouched'or'onBlur', nothing fires until the field has received and lost focus. Checktouchedwith the Field Debug component. - Check Standard Schema support. Zod requires 3.24+.
Types not autocompleting
- Pass the typed instance to
form.create(). Use the instance returned by.addComponent(), not a freshril()call. - Reuse a single instance. Export it from a shared module; recreating it drops accumulated component types.
- TypeScript 5.0+ is required for type-safe chaining.
Workflow persistence not working
- Check the persistence key. Two workflows sharing a key overwrite each other.
- Verify adapter configuration in
.configure({ persistence: { ... } }). - Inspect stored data in DevTools → Application → localStorage.
Form submit not firing
- Check validation. If
useFormSubmitState().isValidisfalse, submission is blocked — inspect the errors in the debug panel. - Check the button. It needs
type="submit"and must be inside the<Form>tree. - Check the handler.
onSubmitmust be passed to<Form>.