Navigation
Navigate between workflow steps with validation, conditions, and skip logic.
Workflow navigation in Rilaykit is fully managed through the useWorkflowContext hook. It handles forward/backward movement, step skipping, validation gating, conditional visibility, and cross-step data manipulation.
Navigation Methods
All navigation methods are available from useWorkflowContext(). They return a Promise<boolean> indicating whether the navigation succeeded.
import { useWorkflowContext } from '@rilaykit/workflow';
function MyNavigation() {
const { goNext, goPrevious, goToStep, skipStep } = useWorkflowContext();
return (
<div className="flex gap-2">
<button onClick={() => goPrevious()}>Back</button>
<button onClick={() => skipStep()}>Skip</button>
<button onClick={() => goNext()}>Next</button>
<button onClick={() => goToStep(0)}>Go to first step</button>
</div>
);
}Method Reference
| Method | Signature | Description |
|---|---|---|
goNext() | () => Promise<boolean> | Validates the current step, runs onAfterValidation if defined, marks the step as passed, then advances to the next visible step. |
goPrevious() | () => Promise<boolean> | Navigates to the previous visible step. Does not trigger validation. |
goToStep(index) | (stepIndex: number) => Promise<boolean> | Jumps to a specific step by its original index (not the visible index). Fails if the step is hidden or out of bounds. |
skipStep() | () => Promise<boolean> | Skips the current step without validation. Only works if allowSkip: true or the step's skippable condition evaluates to true. Fires onStepSkip analytics. |
goNext() is the only navigation method that triggers form validation and onAfterValidation. All other methods move directly without validating.
Navigation Guards
Guards let you check whether a navigation action is possible before attempting it. Use them to conditionally render or disable buttons.
const {
canGoNext,
canGoPrevious,
canGoToStep,
canSkipCurrentStep,
} = useWorkflowContext();| Guard | Signature | Returns true when |
|---|---|---|
canGoNext() | () => boolean | There is a visible step after the current one. |
canGoPrevious() | () => boolean | There is a visible step before the current one. |
canGoToStep(index) | (stepIndex: number) => boolean | The target step is within bounds and currently visible. |
canSkipCurrentStep() | () => boolean | The step has allowSkip: true and the skippable condition evaluates to true. |
function NavigationButtons() {
const { goNext, goPrevious, canGoNext, canGoPrevious } = useWorkflowContext();
return (
<div className="flex justify-between">
<button
onClick={() => goPrevious()}
disabled={!canGoPrevious()}
>
Previous
</button>
<button
onClick={() => goNext()}
disabled={!canGoNext()}
>
Next
</button>
</div>
);
}Automatic Step Skipping
Invisible steps are automatically skipped during navigation. When you call goNext() and the next step in sequence is hidden by a condition, the workflow finds the next visible step and jumps to it. The same applies for goPrevious().
Steps: [1: visible] [2: hidden] [3: hidden] [4: visible]
goNext() from step 1 --> lands on step 4
goPrevious() from step 4 --> lands on step 1If the current step becomes invisible (e.g., a condition changes while viewing it), the workflow automatically relocates to the nearest visible step -- first looking forward, then backward.
Step Conditions
Step conditions control visibility and skippability dynamically based on workflow data. They use the when() condition builder from @rilaykit/core.
StepConditionalBehavior
interface StepConditionalBehavior {
visible?: ConditionConfig; // When false, the step is hidden and skipped
skippable?: ConditionConfig; // When true, the step can be skipped
}Defining Conditions
Use the when() builder to create conditions based on data from any step in the workflow.
import { when } from '@rilaykit/core';
const workflow = rilay
.flow('onboarding', 'Onboarding')
.addStep({
id: 'personal-info',
title: 'Personal Info',
formConfig: personalInfoForm,
})
.addStep({
id: 'company-info',
title: 'Company Info',
formConfig: companyInfoForm,
conditions: {
// Only show this step when accountType is "business"
visible: when('accountType').equals('business').build(),
},
})
.addStep({
id: 'preferences',
title: 'Preferences',
formConfig: preferencesForm,
allowSkip: true,
conditions: {
// Dynamically make skippable when user has existing preferences
skippable: when('hasExistingPreferences').equals(true).build(),
},
});const workflow = rilay
.flow('onboarding', 'Onboarding')
.addStep({
id: 'company-info',
title: 'Company Info',
formConfig: companyInfoForm,
})
.addStepConditions('company-info', {
visible: when('accountType').equals('business').build(),
});Conditions evaluate against a flattened view of all workflow data across all steps. Field values from step personal-info are available by their field ID when evaluating conditions on step company-info.
How Conditions Affect Navigation
visible: false-- The step is excluded from the step list, the stepper, and all navigation.goNext()andgoPrevious()skip over it automatically.skippable: true-- Combined withallowSkip: true, the step can be skipped viaskipStep()or the<WorkflowSkipButton>.
The onAfterValidation Callback
The onAfterValidation callback is a powerful hook that runs after a step's form passes validation but before the navigation to the next step occurs. It is the right place for:
- API calls triggered by the validated data
- Pre-filling subsequent steps with fetched data
- Custom validation that depends on external services
.addStep({
id: 'registration',
title: 'Business Registration',
formConfig: registrationForm,
onAfterValidation: async (stepData, helper, context) => {
// Call an API with the validated data
const companyInfo = await fetchCompanyBySiren(stepData.siren);
// Pre-fill the next step
helper.setNextStepFields({
companyName: companyInfo.name,
address: companyInfo.address,
});
// Or target a specific step by ID
helper.setStepFields('company-details', {
legalForm: companyInfo.legalForm,
});
},
})If onAfterValidation throws an error, navigation is cancelled and the user stays on the current step. Use this to block progression when an API call fails.
Signature
onAfterValidation?: (
stepData: Record<string, any>,
helper: StepDataHelper,
context: WorkflowContext
) => void | Promise<void>;StepDataHelper
The StepDataHelper interface provides clean methods to read and modify data across steps from within onAfterValidation.
interface StepDataHelper {
/** Replace all data for a specific step */
setStepData(stepId: string, data: Record<string, any>): void;
/** Merge specific fields into a step's existing data */
setStepFields(stepId: string, fields: Record<string, any>): void;
/** Read the current data for a step */
getStepData(stepId: string): Record<string, any>;
/** Set a single field value on the next step */
setNextStepField(fieldId: string, value: any): void;
/** Merge multiple field values into the next step's data */
setNextStepFields(fields: Record<string, any>): void;
/** Get all data across all steps */
getAllData(): Record<string, any>;
/** Get the full list of step configurations */
getSteps(): StepConfig[];
}Usage Patterns
onAfterValidation: async (stepData, helper) => {
const result = await lookupCompany(stepData.registrationNumber);
helper.setNextStepFields({
companyName: result.name,
address: result.address,
industry: result.sector,
});
}onAfterValidation: async (stepData, helper) => {
const result = await lookupCompany(stepData.registrationNumber);
// Pre-fill step 3, even though we are on step 1
helper.setStepFields('review', {
summary: `${result.name} - ${result.address}`,
});
}onAfterValidation: async (stepData, helper, context) => {
const personalInfo = helper.getStepData('personal-info');
const allData = helper.getAllData();
// Use data from a previous step to enrich the current call
await enrichProfile({
email: personalInfo.email,
preferences: stepData,
});
}Navigation Flow Summary
Here is the complete flow when the user clicks "Next":
Form validation
The current step's form is validated using its field validators. If validation fails, errors are shown and navigation is blocked.
onAfterValidation
If the step defines onAfterValidation, it is called with the validated data and the StepDataHelper. If it throws, navigation is cancelled.
Step marked as passed
The current step ID is added to the passedSteps set, indicating it has been successfully validated.
Find next visible step
The workflow scans forward from the current index and finds the next step whose visible condition evaluates to true (or has no condition). Hidden steps are skipped.
Transition
The onStepChange callback fires, the current step index updates, and the new step is marked as visited.