react-incremental-funnel
v0.1.2
Published
Reusable primitives for building incremental React funnels
Readme
react-incremental-funnel
TypeScript-first React package for building incremental funnel flows with a small runtime API and exported types.
Installation
npm install react-incremental-funnelBasic hook usage
import { useIncrementalFunnel } from 'react-incremental-funnel';
type FunnelValues = {
fullName?: string;
email?: string;
consent?: boolean;
};
export function BasicFunnel() {
const funnel = useIncrementalFunnel<FunnelValues>({
storageKey: 'example-funnel',
steps: ['start', 'details', 'review']
});
return (
<button
onClick={() => {
funnel.updateValues({ consent: true });
funnel.nextStep();
}}
>
Continue
</button>
);
}Example integration (mock endpoints only)
import { useIncrementalFunnel } from 'react-incremental-funnel';
type FunnelValues = {
fullName?: string;
email?: string;
consent?: boolean;
};
const mockApi = {
async createSession() {
return { sessionId: 'mock-session-id' };
},
async saveProgress(values: Partial<FunnelValues>) {
await fetch('/mock/funnel/progress', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(values)
});
},
async submit(values: Partial<FunnelValues>) {
await fetch('/mock/funnel/submit', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(values)
});
}
};
export function FunnelWithMockApi() {
const funnel = useIncrementalFunnel<
FunnelValues,
'start' | 'details' | 'review'
>({
storageKey: 'example-funnel',
steps: ['start', 'details', 'review'],
createSession: () => mockApi.createSession(),
updateRemote: values => mockApi.saveProgress(values),
submitRemote: values => mockApi.submit(values)
});
return <button onClick={() => void funnel.submit()}>Submit</button>;
}Step orchestration
Use these APIs to control progress through your funnel:
nextStep()/previousStep()to move throughstepsgoToStep(stepId)to jump to a specific stepmarkStepComplete(stepId)/markStepIncomplete(stepId)for explicit completion statecurrentStepId,completedStepIds,canGoNext, andcanGoBackfor UI guardspersistStepState: trueto persist step position across sessionsincludeStepStateInRemoteUpdate: trueto include step state in remote updates
Field-level persistence policies
Use fieldPolicies to control where each field can persist:
local: persist in local storagesession: persist in session storagememory: persist in memory onlyremoteOnly: never persist locally, include only in remote updates/submission
ttlMs can be added per field to expire persisted values automatically.
fieldPolicies: {
fullName: { persist: 'local', ttlMs: 7 * 24 * 60 * 60 * 1000 },
email: { persist: 'session', ttlMs: 2 * 60 * 60 * 1000 },
consent: { persist: 'memory' },
temporaryInput: { persist: 'memory' },
sensitiveDraft: { persist: 'remoteOnly' }
}Storage adapters
Built-in adapters:
createLocalStorageAdapter()createSessionStorageAdapter()createMemoryStorageAdapter()
Override any adapter with storageAdapters:
storageAdapters: {
memory: createMemoryStorageAdapter();
}Remote update callbacks
Use updateRemote(values) (or remoteUpdate({ values, stepState })) to receive debounced in-progress updates.
Pair with lifecycle callbacks:
onRemoteUpdateSucceededonRemoteUpdateFailed
Inspect remoteSyncStatus and lastSuccessfulRemoteSyncAt to drive UI status.
Session creation callbacks
Use createSession() to create a server-side draft/session at funnel start.
Inspect session state with:
sessionCreationStatussessionCreationErrorsessionMetadata
Submit callbacks
Use submitRemote(values) for final submission and call submit() from the hook result.
Inspect submit state with:
submitStatussubmitError
Lifecycle callbacks for submission:
onSubmitStartedonSubmitSucceededonSubmitFailed
Resume / start-again handling
Use saved progress flags:
savedProgressExistssavedProgressIsStalesavedProgressMetadata
Actions:
continueSavedProgress()startAgain()clearSavedProgress()(removes persisted progress only)
Validation callback usage
Provide per-step and full-submit validation callbacks:
validateStep: async (stepId, values) => {
if (stepId === 'details' && !values.email) {
return {
stepErrors: ['Please complete this step'],
fieldErrors: { email: 'Email is required' }
};
}
},
validateAll: async values => {
if (!values.consent) {
return {
stepErrors: ['Please accept before submitting'],
fieldErrors: { consent: 'Consent is required' }
};
}
}Use canContinueCurrentStep, currentStepValidationErrors, and fieldValidationErrors in UI.
Lifecycle event callbacks
You can subscribe to lifecycle events:
onFunnelStartedonStepStartedonStepCompletedonValuesChangedonRemoteUpdateSucceededonRemoteUpdateFailedonSubmitStartedonSubmitSucceededonSubmitFailedonFunnelReset
Set includeValuesInLifecycleCallbacks: true only when you explicitly need values payloads.
Shared/public device guidance
For shared/public devices:
- Prefer
sessionormemorypersistence overlocal - Use short
ttlMsvalues for persisted fields - Mark sensitive fields as
memoryorremoteOnly - Offer a visible “Start again” action that calls
startAgain() - Offer a visible “Clear saved progress” action that calls
clearSavedProgress()
Security and privacy guidance
- Do not store secrets in funnel values.
- Treat local/session storage as user-accessible and non-secret storage.
- Persist only what is required; default sensitive fields to
memoryorremoteOnly. - Redact or minimize telemetry in lifecycle callbacks unless required.
- Validate and sanitize values server-side before trusting updates/submissions.
Development
npm install
npm run lint
npm run test
npm run buildRelease workflow
This package uses Changesets for versioning and changelogs.
Add a changeset in your PR
If your PR changes package behavior, add a changeset:
npm run changesetChoose the bump type:
patch: bug fixes and other backwards-compatible fixes.minor: backwards-compatible features.major: breaking changes.
How releases happen
- Changes merge through pull requests into
main. - On pushes to
main, the Release workflow runschangesets/action. - If unreleased changesets exist, it creates or updates a release PR with:
package.jsonversion updatesCHANGELOG.mdupdates- consumed changesets removed
- When that release PR is merged, the same workflow publishes to npm with:
npm publish --provenance --access public- GitHub OIDC Trusted Publishing (
id-token: write) via GitHub Actions
Do not normally run npm publish from a developer machine.
Stable and prerelease channels
- Stable releases are published from
mainto the defaultlatesttag (for example1.1.0). - If prereleases are needed, use Changesets prerelease mode and publish with a prerelease tag such as
next(for example1.2.0-next.0).
Local package verification
Before release, verify package contents locally:
npm pack --dry-runPublic API
createFunneladvanceFunneluseIncrementalFunnelcreateLocalStorageAdapter,createSessionStorageAdapter,createMemoryStorageAdapterpickPersistableValues,removeBlockedFields,redactValuesFunnelStep,FunnelState,UseIncrementalFunnelOptions,UseIncrementalFunnelResult,FunnelStepId
