Skip to content

Clear abandoned branch data

Two different questions get confused here, so this page separates them:

  • A step went off the route because a condition changed. What it collected is kept.
  • A step says its answers must not travel onward. They are dropped whenever it is left.

Pick Business, fill the company in, then go back and pick Personal. The review step shows what would be submitted.

Who is paying

`when` decides reachability; `clearOnLeave` decides what survives leaving. They are not the same instruction.

flow.ts
import { defineFlow, step } from '@wizzard-packages/core';
import { eq, get } from '@wizzard-packages/core/expr';
/**
* A branch, and one step whose answers must not travel with it.
*
* Two different rules are at work, and the page beside this file is about
* telling them apart:
*
* - `when` decides whether a step is on the route. A visitor who picks
* `personal` never sees `company`, and anything they typed there before
* changing their mind is *kept* - the engine does not throw data away
* because a step stopped being reachable.
* - `clearOnLeave` is the opposite instruction, and it is per step: drop this
* slice when the step is left, whichever way it is left. `coupon` is a code
* that was either applied or was not; carrying it into the submission would
* be keeping a receipt nobody asked for.
*/
export const checkout = defineFlow({
id: 'clear-branch-data',
version: 1,
order: ['plan', 'company', 'coupon', 'review'],
steps: {
plan: step<{ payer: 'personal' | 'business' }>({ label: 'Who is paying' }),
company: step<{ name: string }>({
label: 'Company',
when: eq(get('data.plan.payer'), 'business'),
}),
coupon: step<{ code: string }>({
label: 'Coupon',
clearOnLeave: true,
}),
review: step({ label: 'Review' }),
},
policy: 'free',
});
App.tsx
import {
WizardProvider,
useField,
useNavigation,
useStep,
useWizardSelector,
} from '@wizzard-packages/react';
import { checkout } from './flow';
export function App() {
return (
<WizardProvider flow={checkout}>
<Wizard />
</WizardProvider>
);
}
export function Wizard() {
const { current, isLast } = useStep();
const { next, back, canBack } = useNavigation();
const [payer, setPayer] = useField<string>('plan.payer');
const [company, setCompany] = useField<string>('company.name');
const [code, setCode] = useField<string>('coupon.code');
// What would be submitted, read straight off the engine rather than tracked
// here: this is the whole point of the page, so it should not be a copy.
const data = useWizardSelector((s) => JSON.stringify(s.data, null, 2));
const step = current ?? 'plan';
const starting = current === null;
return (
<form onSubmit={(e) => e.preventDefault()}>
{step === 'plan' && (
<fieldset disabled={starting}>
<legend>Who is paying</legend>
{(['personal', 'business'] as const).map((choice) => (
<label key={choice}>
<input
type="radio"
name="payer"
value={choice}
checked={payer === choice}
onChange={() => setPayer(choice)}
/>
{choice === 'personal' ? 'Personal' : 'Business'}
</label>
))}
</fieldset>
)}
{step === 'company' && (
<label>
Company name
<input
value={company ?? ''}
onChange={(e) => setCompany(e.target.value)}
disabled={starting}
/>
</label>
)}
{step === 'coupon' && (
<label>
Coupon code
<input value={code ?? ''} onChange={(e) => setCode(e.target.value)} disabled={starting} />
</label>
)}
{step === 'review' && (
<>
<p>This is what would be submitted:</p>
<pre>{data}</pre>
</>
)}
<button type="button" onClick={() => back()} disabled={starting || !canBack}>
Back
</button>
<button type="button" onClick={() => next()} disabled={starting || isLast}>
Next
</button>
</form>
);
}

A step whose when is false is off the route: it is skipped forwards and backwards, and it is drawn as a step this visitor will not see. The engine does not throw away what it collected before the condition changed, because it cannot know whether you wanted that. A visitor who switches back to Business finds the company name still there.

If that is not what you want for a particular step, say so on the step:

coupon: step<{ code: string }>({
label: 'Coupon',
clearOnLeave: true,
}),

true drops the whole slice - the step’s own slice, or its id when it has none. A list of paths drops exactly those:

clearOnLeave: ['payment.cvv'],

The rule is whenever the step is left, forwards included. That is right for a one-time code or a coupon that was either applied or was not - things the last step has no business submitting. It is wrong for anything the review step needs, so do not reach for it to tidy up a branch.

clearOnLeave runs on a move away from the step, and only on that. Two things follow, and both matter if you were about to reach for it to keep a card number out of storage.

While the person is still on the step, what they typed is already written. Every edit is a commit, and a persistence plugin writes on every commit. Leaving the step keeps the value out of the next write; it does not reach back into the one that has already happened.

Completion is not a move away. A flow that ends keeps the last step’s data, because that data is the submission.

So for a field that must never be stored, keep it out of the persisted state rather than clearing it afterwards: hold it in the component that collects it, or give the wizard a storage that refuses to write that path.

The flow has the full step table, including slice and when. Expressions is the language a when is written in, and Navigation covers what happens to the back stack when a step stops being reachable.