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-maestro-flow

v0.5.3

Published

Stateful, branching workflow orchestration for React.

Readme

react-maestro-flow

Stateful, branching workflow orchestration for React.

Build complex, multi-step user flows with automatic state management, URL synchronization, and conditional navigation.

Features

  • Graph-based navigation - Define your flow as a graph of nodes with conditional branching
  • Automatic state management - State persists across pages in session storage
  • URL synchronization - Works with query params or path-based routing (Next.js, Remix, etc.)
  • Conditional routing - Navigate based on user state or API responses
  • Skip pages - Automatically skip pages based on conditions
  • Browser history - Proper back/forward navigation with skipped pages handled correctly
  • TypeScript - Full type safety with inferred state types

Installation

npm install react-maestro-flow

Quick Start

import { Flow, initializeFlow, useFlow } from "react-maestro-flow";

// 1. Define your pages as nodes
const nodes = [
  {
    currentPage: 'step1',
    nextPage: 'step2',
  },
  {
    currentPage: 'step2',
    nextPage: (state) => {
      // State is keyed by page: state.step2.userType, state.step1.name, etc.
      const step2 = state.step2 as {userType?: string} | undefined;
      return step2?.userType === 'premium' ? 'premiumStep' : 'standardStep';
    },
  },
  {
    currentPage: 'premiumStep',
    nextPage: 'complete',
  },
  {
    currentPage: 'standardStep',
    nextPage: 'complete',
  },
];

// 2. Create the graph
const graph = initializeFlow(nodes, 'step1');

// 3. Define component loaders
const componentLoaders = new Map([
  ['step1', () => import('./pages/Step1')],
  ['step2', () => import('./pages/Step2')],
  ['premiumStep', () => import('./pages/PremiumStep')],
  ['standardStep', () => import('./pages/StandardStep')],
  ['complete', () => import('./pages/Complete')],
]);

// 4. Use the Flow component
function App() {
  return (
    <Flow
      graph={graph}
      config={{
        componentLoaders,
      }}
    />
  );
}

// 5. In your page components, use the hook
function Step1() {
  const {stateKey, goToNext, hasNext} = useFlow();
  const [name, setName] = stateKey<string>('name');

  return (
    <div>
      <input
        value={name || ''}
        onChange={(e) => setName(e.target.value)}
        placeholder='Enter your name'
      />
      <button onClick={goToNext} disabled={!hasNext}>
        Next
      </button>
    </div>
  );
}

Configuration

Flow Component Props

<Flow
  graph={FlowGraph}
  config={{
    // Optional: URL params adapter (defaults to query params)
    urlParamsAdapter?: UrlParamsAdapter,

    // Optional: URL param names (defaults shown)
    pageParamName?: string, // default: "page"
    uuidParamName?: string, // default: "id"

    // Optional: Callback when page changes (page, previousPage, state)
    onPageChange?: (page: string | null, previousPage: string | null, state: FlowState) => void,

    // Optional: Use internal state (session storage). Default: true.
    // Set to false for navigation-only usage (state in memory, lost on refresh).
    enableState?: boolean,

    // Required: Map of page IDs to component loaders
    componentLoaders: Map<string, ComponentLoader>,
  }}
/>

enableState (default: true)

Controls whether the flow uses the internal state system (session storage).

  • true (default): State is persisted in session storage. Users can refresh or revisit via URL and resume. The "expired" flow runs when the UUID has no stored state.
  • false: State is kept in memory only (lost on refresh). No session storage, no expired check. Use when you only need navigation (goToNext, goToPrevious, goToPage, skipToPage, etc.) and manage state yourself (e.g. React state, URL, or external store).
// Navigation only, no persisted state
<Flow graph={graph} config={{componentLoaders, enableState: false}} />

FlowNode Properties

Each node in your graph can have:

{
  currentPage: string; // Required: Unique page identifier

  nextPage?: string | ((state: FlowState) => string | null);
  // Optional: Next page(s). Can be:
  // - A string: "nextPageId"
  // - A function: (state) => state.condition ? "pageA" : "pageB"

  previousPageFallback?: string;
  // Optional: Fallback when resolving previous non-skipped pages
  // (Back navigation uses browser history by default)

  shouldSkip?: (state: FlowState) => boolean;
  // Optional: Skip this page if function returns true
  // Skipped pages are automatically bypassed
}

useFlow Hook

The useFlow hook provides access to all flow functionality:

const {
  // Current state
  currentPage: string | null,
  state: FlowState, // Accumulated state from all pages

  // Navigation
  goToNext: () => void,
  goToPrevious: () => void,
  goToPage: (page: string) => void, // Preserves history
  skipToPage: (page: string) => void, // Replaces history

  // State management
  stateKey: <T>(key: string) => [T | undefined, (value: T) => void],
  updateState: (key: string, value: unknown) => void,
  updateStateBatch: (updates: Record<string, unknown>) => void,
  getPageState: (page: string) => FlowState,

  // Navigation info
  hasNext: boolean,

  // Utilities
  getCurrentNode: () => FlowNode | undefined,
  getNode: (page: string) => FlowNode | undefined,
  skipCurrentPage: () => void,
  completeFlow: () => void,

  // URL params
  getUrlParam: (key: string) => string | null,
  getAllUrlParams: () => Record<string, string>,
  urlParams: Record<string, string>,
} = useFlow();

State Management

Using stateKey (Recommended)

const [name, setName] = stateKey<string>('name');
const [email, setEmail] = stateKey<string>('email');

// Use like useState
<input value={name || ''} onChange={(e) => setName(e.target.value)} />;

Using updateState / updateStateBatch

updateState('name', 'John');
updateStateBatch({name: 'John', email: '[email protected]'});

Getting State

// Get all accumulated state
const allState = state; // { name: "John", email: "...", ... }

// Get state for a specific page
const step1State = getPageState('step1');

Navigation

Basic Navigation

// Go to next page
goToNext();

// Go to previous page (uses browser history)
goToPrevious();

// Check if navigation is possible
if (hasNext) {
  goToNext();
}

Jumping to Specific Pages

// Navigate to any page (preserves history - user can go back)
goToPage('step5');

// Skip to a page (replaces history - no back button)
// Useful after API calls
const result = await checkEligibility();
if (result.eligible) {
  skipToPage('checkout');
} else {
  skipToPage('notEligible');
}

Conditional Skipping

// Skip current page programmatically (e.g., after async check)
useEffect(() => {
  checkStatus().then((status) => {
    if (status.shouldSkip) {
      skipCurrentPage();
    }
  });
}, []);

URL Parameters

Access arbitrary URL parameters (works with query params or path params):

// Get a single param
const userId = getUrlParam('userId');

// Get all params
const allParams = getAllUrlParams(); // { page: "step1", id: "abc123", userId: "456" }

// Reactive params (updates on navigation)
const {userId, type} = urlParams;

Advanced Usage

Path-Based URLs (Next.js, Remix, etc.)

import { createPathParamsAdapter, Flow } from "react-maestro-flow";

// For Next.js App Router
const adapter = createPathParamsAdapterFromProps(
  params, // from page props
  {template: '/flow/[id]/[page]'},
);

// For custom routing
const adapter = createPathParamsAdapter({
  template: '/[id]/page/[page]',
});

<Flow
  graph={graph}
  config={{
    componentLoaders,
    urlParamsAdapter: adapter,
    pageParamName: 'page',
    uuidParamName: 'id',
  }}
/>;

Conditional Routing

const nodes = [
  {
    currentPage: 'checkout',
    nextPage: (state) => {
      if (state.paymentMethod === 'credit') return 'creditCardForm';
      if (state.paymentMethod === 'paypal') return 'paypalFlow';
      return 'paymentSelection';
    },
  },
];

Skipping Pages

const nodes = [
  {
    currentPage: 'optionalStep',
    shouldSkip: (state) => !state.needsOptionalStep,
    nextPage: 'nextStep',
  },
];

Page Change Callback

onPageChange receives the new page, previous page, and accumulated state. It fires on initial load (with previousPage as null) and on every navigation. Use it to sync parent state or track analytics:

<Flow
  graph={graph}
  config={{
    componentLoaders,
    onPageChange: (newPage, previousPage, state) => {
      setCurrentPage(newPage); // Sync with parent state
      analytics.track('page_view', {page: newPage});
      // state is merged from all pages - use for summaries, etc.
    },
  }}
/>

Completing the flow

function CompletePage() {
  const {completeFlow} = useFlow();

  const handleComplete = () => {
    completeFlow(); // Clears session storage
    // Navigate away or show success message
    router.push('/thank-you');
  };

  return <button onClick={handleComplete}>Finish</button>;
}

API Reference

Types

FlowNode<TState>

type FlowNode<TState = FlowState> = {
  currentPage: string;
  nextPage?: string | ((state: TState) => string | null);
  previousPageFallback?: string;
  shouldSkip?: (state: TState) => boolean;
};

FlowGraph

type FlowGraph = {
  nodes: Map<string, FlowNode>;
  entryPoint?: string;
};

FlowConfig

type FlowConfig = {
  urlParamsAdapter?: UrlParamsAdapter;
  pageParamName?: string; // default: "page"
  uuidParamName?: string; // default: "id"
  onPageChange?: (
    page: string | null,
    previousPage: string | null,
    state: FlowState,
  ) => void;
  enableState?: boolean; // default: true
  componentLoaders: Map<string, ComponentLoader>;
};

Graph Utilities

// Create a graph from nodes
const graph = initializeFlow(nodes, entryPoint?);

// Validate graph structure
const { valid, errors } = validateGraph(graph);

// Get next/previous pages
const nextPage = getNextPage(graph, currentPage, state);
const previousPage = getPreviousPage(graph, currentPage, state);

// Get all pages in order
const pages = getPagesInOrder(graph);

Examples

See the playground/ directory for complete examples including:

  • Basic multi-step form
  • Conditional routing
  • Path-based URLs
  • Query param URLs
  • Page skipping

License

MIT