rilaykit
Workflow

Workflow Hooks

Zustand-powered hooks for granular workflow state access and optimal performance.

Like forms, RilayKit workflows use Zustand stores under the hood, exposing granular selector hooks so your components only re-render when the specific state slice they depend on changes. Instead of subscribing to the entire workflow state, you pick the minimal data you need.

All hooks documented on this page are imported from @rilaykit/workflow.

import {
  useWorkflowContext,
  useCurrentStepIndex,
  useWorkflowActions,
  // ...
} from '@rilaykit/workflow';

Context Hook

useWorkflowContext() returns the full workflow context object. It is the most convenient hook for top-level orchestration components, but because it subscribes to the entire state, it will re-render on every state change.

import { useWorkflowContext } from '@rilaykit/workflow';

function WorkflowOrchestrator() {
  const workflow = useWorkflowContext();

  return (
    <div>
      <p>Step {workflow.workflowState.currentStepIndex + 1} of {workflow.context.totalSteps}</p>
      <button onClick={() => workflow.goNext()}>Next</button>
    </div>
  );
}

Prefer the granular hooks described below for leaf components. Reserve useWorkflowContext() for top-level layouts that already depend on many state slices.

Return type

The hook returns an object with the following shape:

interface WorkflowContextValue {
  workflowState: {
    currentStepIndex: number;
    allData: Record<string, unknown>;
    stepData: Record<string, unknown>;
    visitedSteps: Set<string>;
    passedSteps: Set<string>;
    isSubmitting: boolean;
    isTransitioning: boolean;
    isInitializing: boolean;
  };
  workflowConfig: WorkflowConfig;
  currentStep: StepConfig;
  context: WorkflowContext;
  formConfig?: FormConfiguration;
  conditionsHelpers: UseWorkflowConditionsReturn;
  currentStepMetadata?: Record<string, unknown>;

  // Navigation
  goToStep(stepIndex: number): Promise<boolean>;
  goNext(): Promise<boolean>;
  goPrevious(): Promise<boolean>;
  skipStep(): Promise<boolean>;
  canGoToStep(stepIndex: number): boolean;
  canGoNext(): boolean;
  canGoPrevious(): boolean;
  canSkipCurrentStep(): boolean;

  // Data
  setValue(fieldId: string, value: unknown): void;
  setStepData(data: Record<string, unknown>): void;
  resetWorkflow(): void;

  // Submission
  submitWorkflow(): Promise<void>;
  isSubmitting: boolean;
  canSubmit: boolean;

  // Persistence
  persistNow?: () => Promise<void>;
  isPersisting?: boolean;
  persistenceError?: Error | null;
}

Granular State Hooks

These hooks subscribe to a single slice of the workflow store. A component using useCurrentStepIndex() will only re-render when the step index changes, not when field data or submission state updates.

HookReturnsRe-renders when
useCurrentStepIndex()numberStep changes
useWorkflowTransitioning()booleanTransition state changes
useWorkflowInitializing()booleanInit state changes
useWorkflowSubmitting()booleanSubmit state changes
useWorkflowAllData()Record<string, unknown>Any data changes
useWorkflowStepData()Record<string, unknown>Current step data changes
useStepDataById(stepId)Record<string, unknown> | undefinedSpecific step data changes
useVisitedSteps()Set<string>Visited steps change
usePassedSteps()Set<string>Passed steps change
useIsStepVisited(stepId)booleanStep visit state changes
useIsStepPassed(stepId)booleanStep pass state changes
useWorkflowNavigationState(){ currentStepIndex, isTransitioning, isSubmitting }Navigation state changes
useWorkflowSubmitState(){ isSubmitting, isTransitioning, isInitializing }Submit-related state changes

Usage examples

import {
  useCurrentStepIndex,
  useWorkflowTransitioning,
  useWorkflowSubmitting,
  useIsStepPassed,
} from '@rilaykit/workflow';

function StepIndicator({ stepId, stepIndex }: { stepId: string; stepIndex: number }) {
  const currentIndex = useCurrentStepIndex();
  const isPassed = useIsStepPassed(stepId);

  const isCurrent = currentIndex === stepIndex;

  return (
    <div className={isCurrent ? 'step-active' : isPassed ? 'step-passed' : 'step-pending'}>
      Step {stepIndex + 1}
    </div>
  );
}

function SubmitButton() {
  const isSubmitting = useWorkflowSubmitting();
  const isTransitioning = useWorkflowTransitioning();

  return (
    <button disabled={isSubmitting || isTransitioning}>
      {isSubmitting ? 'Submitting...' : 'Submit'}
    </button>
  );
}

Action Hook

useWorkflowActions() returns an object containing all store mutation functions. It does not subscribe to any state, so the component calling it will not re-render when the store changes.

import { useWorkflowActions } from '@rilaykit/workflow';

const actions = useWorkflowActions();

Available actions:

  • actions.setCurrentStep(index) -- Set the active step by index.
  • actions.setStepData(data, stepId) -- Replace the data for a given step.
  • actions.setAllData(data) -- Replace the entire workflow data object.
  • actions.setFieldValue(fieldId, value, stepId) -- Set a single field value in a specific step.
  • actions.setSubmitting(bool) -- Toggle the submitting flag.
  • actions.setTransitioning(bool) -- Toggle the transitioning flag.
  • actions.setInitializing(bool) -- Toggle the initializing flag.
  • actions.markStepVisited(stepId) -- Mark a step as visited.
  • actions.markStepPassed(stepId) -- Mark a step as passed.
  • actions.reset() -- Reset the entire workflow store to its initial state.
  • actions.loadPersistedState(state) -- Hydrate the store from a previously persisted state.
import { useWorkflowActions } from '@rilaykit/workflow';

function AdminResetButton() {
  const { reset } = useWorkflowActions();

  return <button onClick={reset}>Reset Workflow</button>;
}

Low-level API

useWorkflowActions() exposes the raw Zustand store mutations. For most navigation and data operations, prefer the higher-level methods from useWorkflowContext() (such as goNext() or setValue()), which handle validation, transitions, and side effects automatically.

Step Metadata Hook

useStepMetadata() provides convenient accessors for step-level metadata defined in your workflow configuration. This is useful for conditional rendering based on arbitrary metadata you attach to steps.

import { useStepMetadata } from '@rilaykit/workflow';

const metadata = useStepMetadata();

Available properties and methods:

  • metadata.current -- The metadata object for the current step.
  • metadata.getByStepId(stepId) -- Get metadata for a step by its ID.
  • metadata.getByStepIndex(index) -- Get metadata for a step by its index.
  • metadata.hasCurrentKey(key) -- Check if the current step's metadata contains a key.
  • metadata.getCurrentValue<T>(key, defaultValue?) -- Get a typed value from the current step's metadata, with an optional default.
  • metadata.getAllStepsMetadata() -- Get metadata for all steps.
  • metadata.findStepsByMetadata(predicate) -- Find steps whose metadata matches a predicate function.
import { useStepMetadata } from '@rilaykit/workflow';

function StepLayout({ children }: { children: React.ReactNode }) {
  const metadata = useStepMetadata();

  const showSidebar = metadata.getCurrentValue<boolean>('showSidebar', false);
  const helpText = metadata.getCurrentValue<string>('helpText');

  return (
    <div className="step-layout">
      <main>{children}</main>
      {showSidebar && (
        <aside>
          {helpText && <p>{helpText}</p>}
        </aside>
      )}
    </div>
  );
}

Condition Hooks

useWorkflowConditions() evaluates step and field conditions defined in your workflow configuration. It returns visibility, skip, and field-level condition results for the entire workflow.

import { useWorkflowConditions } from '@rilaykit/workflow';

const conditions = useWorkflowConditions({
  workflowConfig,
  workflowState,
  currentStep,
});

Return value

  • stepConditions -- { visible: boolean; skippable: boolean } for the current step.
  • fieldConditions -- Record<string, ConditionEvaluationResult> with condition results for each field.
  • allStepConditions -- Record<number, StepConditionResult> with condition results keyed by step index.

Helper methods

  • isStepVisible(index) -- Whether a step should be rendered.
  • isStepSkippable(index) -- Whether a step can be skipped.
  • isFieldVisible(fieldId) -- Whether a field should be rendered.
  • isFieldDisabled(fieldId) -- Whether a field should be disabled.
  • isFieldRequired(fieldId) -- Whether a field is required.
  • isFieldReadonly(fieldId) -- Whether a field is read-only.
import { useWorkflowContext } from '@rilaykit/workflow';

function ConditionalField({ fieldId, children }: { fieldId: string; children: React.ReactNode }) {
  const { conditionsHelpers } = useWorkflowContext();

  if (!conditionsHelpers.isFieldVisible(fieldId)) {
    return null;
  }

  return <div data-readonly={conditionsHelpers.isFieldReadonly(fieldId)}>{children}</div>;
}

When using useWorkflowContext(), the condition helpers are already available via conditionsHelpers. You only need to call useWorkflowConditions() directly when building components outside of the standard workflow provider tree.

Best Practices

  • Use granular hooks for leaf components. A step indicator only needs useCurrentStepIndex() and useIsStepPassed(). Subscribing to the full context would cause unnecessary re-renders every time any data changes.
  • Use useWorkflowContext() for top-level orchestration. If a component already depends on navigation, data, and submission state, there is no benefit in splitting across multiple granular hooks.
  • Use useWorkflowActions() for programmatic state changes. Since it does not subscribe to state, components that only dispatch actions (like a reset button) will never re-render due to store changes.
  • Use useStepMetadata() for conditional rendering. Attaching metadata to steps (e.g., showSidebar, requiresAuth) and reading it through this hook keeps your rendering logic declarative and decoupled from step indices.

On this page