npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-funnel

Basic 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 through steps
  • goToStep(stepId) to jump to a specific step
  • markStepComplete(stepId) / markStepIncomplete(stepId) for explicit completion state
  • currentStepId, completedStepIds, canGoNext, and canGoBack for UI guards
  • persistStepState: true to persist step position across sessions
  • includeStepStateInRemoteUpdate: true to include step state in remote updates

Field-level persistence policies

Use fieldPolicies to control where each field can persist:

  • local: persist in local storage
  • session: persist in session storage
  • memory: persist in memory only
  • remoteOnly: 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:

  • onRemoteUpdateSucceeded
  • onRemoteUpdateFailed

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:

  • sessionCreationStatus
  • sessionCreationError
  • sessionMetadata

Submit callbacks

Use submitRemote(values) for final submission and call submit() from the hook result.

Inspect submit state with:

  • submitStatus
  • submitError

Lifecycle callbacks for submission:

  • onSubmitStarted
  • onSubmitSucceeded
  • onSubmitFailed

Resume / start-again handling

Use saved progress flags:

  • savedProgressExists
  • savedProgressIsStale
  • savedProgressMetadata

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:

  • onFunnelStarted
  • onStepStarted
  • onStepCompleted
  • onValuesChanged
  • onRemoteUpdateSucceeded
  • onRemoteUpdateFailed
  • onSubmitStarted
  • onSubmitSucceeded
  • onSubmitFailed
  • onFunnelReset

Set includeValuesInLifecycleCallbacks: true only when you explicitly need values payloads.

Shared/public device guidance

For shared/public devices:

  • Prefer session or memory persistence over local
  • Use short ttlMs values for persisted fields
  • Mark sensitive fields as memory or remoteOnly
  • 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 memory or remoteOnly.
  • 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 build

Release 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 changeset

Choose 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 runs changesets/action.
  • If unreleased changesets exist, it creates or updates a release PR with:
    • package.json version updates
    • CHANGELOG.md updates
    • 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 main to the default latest tag (for example 1.1.0).
  • If prereleases are needed, use Changesets prerelease mode and publish with a prerelease tag such as next (for example 1.2.0-next.0).

Local package verification

Before release, verify package contents locally:

npm pack --dry-run

Public API

  • createFunnel
  • advanceFunnel
  • useIncrementalFunnel
  • createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter
  • pickPersistableValues, removeBlockedFields, redactValues
  • FunnelStep, FunnelState, UseIncrementalFunnelOptions, UseIncrementalFunnelResult, FunnelStepId