use-funnel

repository·main·Indexed 20 days ago

https://github.com/toss/use-funnel

A React-based state management library for handling complex, multi-step user flows (funnels). It features strong type safety, history-based navigation for intuitive back/forward movement, and support for various routing environments including browser history, react-router, Next.js, and @react-navigation/native. The library provides tools for runtime context validation via Zod or Superstruct and allows for custom router implementation using @use-funnel/core.

Tokens
27.2K
Snippets
68
Records
87
Agent score
64%

What's inside use-funnel

  1. What is @use-funnel?

    main
    @use-funnel is a step-by-step state management library optimized for mobile navigation. It is designed to handle complex navigation patterns, such as back/forward transitions in mobile apps, by managing state through a history-based approach. It provides strong type support to validate transitions between steps, ensuring that only valid and required states are requested during the navigation flow.
  2. Manage history and state snapshots with useFunnel

    main

    Unlike manual state management where history (navigating back/forward) and UI state are often decoupled, @use-funnel manages them together.

    Every time you transition to a new step via funnel.history.push, the library saves a state snapshot. When a user navigates back in the history, @use-funnel restores the exact state snapshot associated with that previous step. This ensures that user inputs or selections made in previous steps are preserved correctly when navigating through a multi-step flow.

  3. Core concepts of @use-funnel

    main

    To use @use-funnel effectively, you should understand its three primary abstractions:

    • step: Represents an individual screen or stage within the UI flow (e.g., an input screen for 'Occupation').
    • context: Represents the data or values entered during the flow (e.g., the specific occupation value selected by the user).
    • history: Represents the complete record of the user's navigation path, including all screens visited and the values entered at each step.
  4. Key features of @use-funnel

    main

    @use-funnel offers several core capabilities for managing complex user flows:

    • State Management by History: Manages state transitions through a history stack, making it easy to handle mobile-style back/forward navigation.
    • Strong Type Support: Provides type-safe state management that validates transitions between steps, preventing invalid state requests.
    • Various Router Support: Optimized for mobile environments with @react-navigation/native support, while also integrating seamlessly with react-router and Next.js.
  5. Core concepts of @use-funnel

    main

    To use @use-funnel effectively, understand these three pillars:

    1. Strong Type Support: The library compares the type of the current step with the next step. This ensures that only the required states/contexts are managed safely, preventing invalid transitions between steps.
    2. State Management by History: Instead of a single monolithic state, @use-funnel manages state based on a history stack. This makes implementing backward and forward navigation (e.g., a 'Back' button) intuitive and consistent.
    3. Various Router Support: It is designed to work across different routing environments, including browser history, react-router, next.js, and @react-navigation/native.
  6. Compare history.push() vs history.replace()

    main

    Understanding the difference between these two methods is critical for managing history length and back-button behavior.

    Featurehistory.push()history.replace()
    History entryAdds a new entryOverwrites current entry
    currentIndexIncremented by 1No change
    On back()Moves to previous entryPrevious value is already overwritten
    Primary Use CaseNavigating to the next stepSaving current step's state or skipping steps

    Example of skipping a step: If you use history.push('B') followed by history.replace('C'), the history sequence becomes A -> C. Navigating back from C will return the user to A, skipping B entirely. This is useful for intermediate steps like loading screens.

  7. How useFunnel provides strong type safety for step context

    main

    In @use-funnel, the context property is dynamically typed based on the current step. This means that as you narrow down the step using type guards (e.g., if (funnel.step === 'B')), the TypeScript compiler automatically knows the exact shape of funnel.context for that specific step.

    This solves the problem of "leaky" state requirements where transitioning from Step A to Step B might require properties that were only relevant to Step A, or conversely, failing to provide properties required by Step B.

    const funnel = useFunnel<{
      A: { a?: string; b?: string };
      B: { a: string; b?: string };
    }>({
      id: "strongly-typed",
      initial: { step: "A" }
    });
    
    // When the step is "A", context.a is string | undefined
    if (funnel.step === "A") {
      console.log(typeof funnel.context.a); // "string" | "undefined"
    }
    
    // When the step is "B", context.a is strictly string
    if (funnel.step === "B") {
      console.log(typeof funnel.context.a); // "string"
    }
  8. Use overlays for modals and bottom sheets

    main

    The funnel.Render.overlay method allows you to display UI components like modals or bottom sheets while keeping the previous step visible in the background. This is useful for transient inputs that don't require a full screen transition.

    When using an overlay, the render function provides a close method. If the overlay is closed via user interaction (e.g., clicking a backdrop or a cancel button) rather than the browser's back button, you must explicitly call close() to navigate the funnel history back to the previous step.

    <funnel.Render
      PreviousStep={({ history }) => (
        <PreviousStep onNext={() => history.push('OverlayStep')} />
      )}
      OverlayStep={funnel.Render.overlay({
        render({ history, close }) {
          return (
            <MyModal
              onNext={(data) => history.push('NextStep', { data })}
              onClose={() => close()} // Explicitly call close() to navigate back in history
            />
          );
        }
      })}
    />
  9. How to create sub-funnels within a funnel

    main

    You can divide a complex funnel into smaller, manageable sub-funnels or reuse specific sequences of steps in different contexts. This is achieved by nesting a new funnel instance inside a step of a parent funnel.

    To implement a sub-funnel:

    1. Inside a component rendered by a parent funnel's step, call useFunnel() again.
    2. Crucially, provide a unique id to the sub-funnel configuration to distinguish its history and state from the parent funnel.
    3. Pass necessary data from the parent context to the sub-funnel via props, and use callbacks (like onNext) to communicate the sub-funnel's completion back to the parent funnel's history.

    This approach allows for modularity and prevents the main funnel's type definition from becoming overly bloated with every intermediate step of a sub-process.

    import { useFunnel } from '@use-funnel/react-router-dom';
    
    // Parent Funnel
    const mainFunnel = useFunnel({
      id: 'main-funnel',
      initial: { step: 'A' }
    });
    
    // Inside a step of the parent funnel (e.g., Step B)
    // We render a component that manages its own sub-funnel
    function BStep({ context }) {
      return <BSubFunnel a={context.a} onComplete={(val) => mainFunnel.history.push('C', { b: val })} />;
    }
    
    // Sub-funnel Component
    function BSubFunnel({ a, onComplete }) {
      const subFunnel = useFunnel({
        id: 'b-funnel', // Unique ID is required
        initial: { step: 'B1' }
      });
    
      return subFunnel.Render({
        B1: ({ history }) => <button onClick={() => history.push('B2')}>Next</button>,
        // ... other steps
        B3: ({ context }) => <button onClick={() => onComplete(context.someValue)}>Finish Sub-funnel</button>
      });
    }
  10. Manage multi-step UI state with useFunnel

    main

    The useFunnel hook allows you to define a multi-step workflow where each step has its own unique, strongly-typed context. This prevents common errors in manual state management where required properties for future steps are accidentally omitted or incorrectly typed during transitions.

    When defining a funnel, you provide a generic object where keys are the step names and values are the shape of the context for that step. @use-funnel ensures that when you transition to a new step, you provide the exact context required by that step's definition.

    const funnel = useFunnel<{
      A: { a?: string; b?: string };
      B: { a: string; b?: string };
    }>({
      id: "strongly-typed",
      initial: {
        step: "A",
      }
    });
    
    // Type safety in action:
    if (funnel.step === "A") {
      // Transitioning to B requires providing 'a' because it is required in step B's context
      funnel.history.push("B", { a: "some value" });
    }
  11. Define and use Transition Events in a funnel

    main

    Transition events allow you to define and control how a funnel transitions from the current step to the next based on specific conditions or user actions. This is the recommended way to handle multiple paths (branching logic) within a single step, such as handling success vs. failure scenarios.

    To implement transition events:

    1. Define an events object within the step configuration passed to funnel.Render.with().
    2. In the events object, map event names to handler functions. These handlers receive a payload and a history object, allowing you to call history.push() to navigate.
    3. In the render function, use the provided dispatch function to trigger the defined events.

    Important: Do not attempt to use history directly inside the render function. Always use dispatch('eventName', payload) to trigger the logic defined in your events object.

    import { useFunnel } from "@use-funnel/next";
    
    const funnel = useFunnel(/* ... */);
    
    <funnel.Render
      EmailInput={funnel.Render.with({
        events: {
          // Email input success event
          EmailInputSuccess: (email: string, { history }) => {
            // Transition to the password input step
            history.push('PasswordInput', { email });
          },
          // Email input fail event
          EmailInputFail: (error: Error, { history }) => {
            // Transition to the error page
            history.push('ErrorPage', { error: error.message });
          }
        },
        render({ context, dispatch }) {
          return (
            <EmailInput
              email={context.email}
              // Dispatch EmailInputSuccess event when email input is successful
              onNext={(email) => dispatch('EmailInputSuccess', email)}
              // Dispatch EmailInputFail event when email input fails
              onError={(error) => dispatch('EmailInputFail', error)}
            />
          );
        }
      })}
    />