Skip to content

Lifecycle Hooks ​

ink-stepper provides lifecycle hooks that allow you to execute logic when steps are entered or exited. This is useful for analytics, saving data, or performing cleanup.

Entering a Step ​

The onEnterStep callback is triggered whenever the active step changes. It receives the index of the new step.

tsx
<Stepper
  onEnterStep={(stepIndex) => {
    console.log(`Navigated to step ${stepIndex}`);
    analytics.trackView(`step_${stepIndex}`);
  }}
>
  {/* ... */}
</Stepper>

Exiting a Step ​

The onExitStep callback is triggered before leaving the current step. It can be used to validate data, save state, or prevent navigation.

tsx
<Stepper
  onExitStep={async (stepIndex) => {
    console.log(`Leaving step ${stepIndex}`);

    // Perform cleanup or save - no return value needed
    await saveData(stepIndex);
  }}
>
  {/* ... */}
</Stepper>

The signature is (step: number) => void | boolean | Promise<void | boolean>. Only an explicit false cancels navigation, so a handler that just performs a side effect can return nothing.

Preventing Navigation ​

If onExitStep returns false (or a Promise that resolves to false), the navigation is cancelled, and the user remains on the current step. This applies to goNext(), goBack() and goTo() alike.

tsx
<Stepper
  onExitStep={(step) => {
    if (step === 0 && !formIsValid) {
      console.log('Cannot leave step 0 yet!');
      return false;
    }
    return true;
  }}
>
  {/* ... */}
</Stepper>

Callback Order ​

Every user-initiated navigation runs the same sequence:

onExitStep(from)  →  onStepChange(to)  →  onEnterStep(to)

goNext() additionally resolves canProceed before any of it; if the check fails, nothing fires. Reaching the end of the wizard calls onComplete instead of the change/enter pair, and going back from the first step calls onCancel.

onExitStep runs before those terminal callbacks too:

onExitStep(last)   →  onComplete()      // advancing past the last step
onExitStep(0)      →  onCancel()        // going back from the first step

So returning false from onExitStep blocks completion and cancellation exactly the same way it blocks a step change — the wizard stays where it is and neither onComplete nor onCancel fires.

Programmatic Jumps with goTo ​

goTo(index) fires the same full lifecycle as goNext/goBack, so onExitStep can cancel a jump by returning false. Two things make it different:

  • It skips canProceed on the current step — goTo is a raw jump, not a validated advance. Use it for "back to summary" style navigation, not to bypass validation on the way forward.
  • The index is clamped to the valid range, and a jump to the current index is a no-op (no callbacks fire).
tsx
import { Text, useInput } from 'ink';

function EditFirstHint({ onEdit }: { onEdit: () => void }) {
  useInput((input) => {
    if (input === '1') onEdit();
  });

  return <Text dimColor>Press "1" to edit the first step.</Text>;
}

<Step name="Review">
  {({ goTo }) => <EditFirstHint onEdit={() => goTo(0)} />}
</Step>

The Review step of examples/wizard.tsx is exactly this: press 1 to goTo(0) and watch the lifecycle log.

Like goNext and goBack, goTo does nothing while async validation is in flight or while navigation is disabled via useStepperInput.

Errors in Callbacks ​

If onExitStep throws or returns a rejecting Promise, navigation is blocked and the error is passed to the onError prop (or logged via console.error when that prop is omitted). See Validation.

Silent Index Repairs ​

Conditional steps can appear and disappear. When that happens, the Stepper keeps the user on the same step by re-pointing the internal index at the step they were already on. If that step itself was removed, the index stays put — whichever step slides into that position becomes active — and only clamps to the last remaining step when the removed step was the last one.

These repairs are not navigation: onExitStep, onStepChange and onEnterStep do not fire for them. Only user-initiated goNext()/goBack()/goTo() calls and keyboard navigation trigger the lifecycle.

Released under the MIT License.