rilaykit
Guides

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.

lib/monitoring.ts
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.

components/form-debug-panel.tsx
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>).

components/workflow-debug-panel.tsx
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.

components/field-debug.tsx
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' }); // false

Performance 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());
MethodDescription
.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 id in .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: [...] } — the validate key is required.
  • Check the validation mode. The default mode is '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. Check touched with 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 fresh ril() 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().isValid is false, 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. onSubmit must be passed to <Form>.

On this page