### Install @use-funnel/next Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/migration.mdx Install the new library using npm or yarn. ```shell npm install @use-funnel/next --save ``` -------------------------------- ### Install @use-funnel/core Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/custom-router.mdx Install the core package for @use-funnel. This is a prerequisite for implementing custom routers. ```shell npm install @use-funnel/core --save ``` -------------------------------- ### Install @use-funnel Adapters Source: https://context7.com/toss/use-funnel/llms.txt Choose the installation command that matches your project's routing solution. These packages provide adapters for different environments. ```shell npm install @use-funnel/browser --save # plain browser history / Next.js App Router ``` ```shell npm install @use-funnel/next --save # Next.js Pages Router ``` ```shell npm install @use-funnel/react-router --save # React Router v6+ ``` ```shell npm install @use-funnel/react-router-dom --save # React Router DOM v6+ ``` ```shell npm install @use-funnel/react-navigation-native --save # React Navigation ``` -------------------------------- ### Run Development Server Source: https://github.com/toss/use-funnel/blob/main/docs/README.md Commands to start the Next.js development server using different package managers. Ensure you are in the project root directory. ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` -------------------------------- ### Overlay Rendering Example Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/funnel-render.mdx Example of how to render a funnel overlay with custom content and event handling. ```APIDOC ### Example ```tsx history.push('PasswordInput', { email })} onClose={() => close()} /> ); } })} /> ``` ``` -------------------------------- ### Basic Usage of useFunnel with @react-navigation/native Source: https://github.com/toss/use-funnel/blob/main/packages/react-navigation-native/README.md Demonstrates how to initialize and use the useFunnel hook with a defined set of steps and their associated contexts. This example shows how to render different components based on the current step and manage navigation events. ```tsx import { useFunnel } from '@use-funnel/react-navigation-native'; export function App() { const funnel = useFunnel<{ SelectJob: { jobType?: 'STUDENT' | 'EMPLOYEE' }; SelectSchool: { jobType: 'STUDENT'; school?: string }; SelectEmployee: { jobType: 'EMPLOYEE'; company?: string }; EnterJoinDate: { jobType: 'STUDENT'; school: string } | { jobType: 'EMPLOYEE'; company: string }; Confirm: ({ jobType: 'STUDENT'; school: string } | { jobType: 'EMPLOYEE'; company: string }) & { joinDate: string }; }>({ id: 'hello-world', initial: { step: 'SelectJob', context: {}, }, }); return ( history.push('SelectSchool', { jobType: 'STUDENT' }), selectEmployee: (_, { history }) => history.push('SelectEmployee', { jobType: 'EMPLOYEE' }), }, render({ dispatch }) { return ( dispatch('selectSchool')} onSelectEmployee={() => dispatch('selectEmployee')} /> ); }, })} SelectSchool={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, school, })) } /> )} SelectEmployee={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, company, })) } /> )} EnterJoinDate={funnel.Render.overlay({ render({ history, close }) { return ( history.push('Confirm', { joinDate })} onClose={() => close()} /> ); }, })} Confirm={({ context }) => context.jobType === 'STUDENT' ? ( ) : ( ) } /> ); } declare function SelectJob(props: { onSelectSchool(): void; onSelectEmployee(): void }): JSX.Element; declare function SelectSchool(props: { onNext(school: string): void }): JSX.Element; declare function SelectEmployee(props: { onNext(company: string): void }): JSX.Element; declare function EnterJoinDateBottomSheet(props: { onNext(joinDate: string): void; onClose(): void }): JSX.Element; declare function ConfirmStudent(props: { school: string; joinDate: string }): JSX.Element; declare function ConfirmEmployee(props: { company: string; joinDate: string }): JSX.Element; ``` -------------------------------- ### useFunnel History Management Example Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/use-funnel.mdx Shows how to use the history.push method within the useFunnel hook to navigate to the next step and provide context. ```typescript import { useFunnel } from "@use-funnel/next"; type T = { helloStep: { message: string; }; worldStep: { message: string; message2: string } } const funnel = useFunnel({ id: "use-funnel-history-example", steps: {}, initial: { step: "helloStep", context: { message: 'Hello' } }, }); if (funnel.step === 'helloStep') { funnel.history.push('worldStep', { message2: 'World' }); } ``` -------------------------------- ### Install Dependencies with pnpm Source: https://github.com/toss/use-funnel/blob/main/CONTRIBUTING.md Install project dependencies using pnpm. It's recommended to use corepack to manage the pnpm version. ```shell corepack enable && corepack prepare pnpm install ``` -------------------------------- ### Basic Funnel Navigation Without Value Preservation Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/examples.mdx This example shows a typical funnel setup where navigation occurs using history.push(). It demonstrates a scenario where values are not preserved on back navigation due to the nature of push. ```tsx function MyFunnel() { const funnel = useFunnel({ id: 'sign-up', initial: { step: 'EmailInput', context: {} }, }); return ( ( history.push('PasswordInput', { email })} /> )} PasswordInput={({ context, history }) => ( history.push('Done', { ...context, password })} onBack={() => history.back()} /> )} Done={({ context }) => (

Signed up as {context.email}

)} /> ); } ``` -------------------------------- ### Install @use-funnel Packages for Routers Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/install.mdx Install the appropriate @use-funnel package based on your project's router. Use npm2yarn for compatibility. ```shell npm install @use-funnel/react-router --save npm install @use-funnel/react-router-dom --save npm install @use-funnel/next --save npm install @use-funnel/react-navigation-native --save npm install @use-funnel/browser --save ``` -------------------------------- ### Basic @use-funnel Hook Usage Source: https://github.com/toss/use-funnel/blob/main/README.md Demonstrates the core setup and rendering of steps using the useFunnel hook. Ensure all declared steps are rendered to avoid runtime errors. ```tsx import { useFunnel } from '@use-funnel/browser'; export function App() { const funnel = useFunnel<{ SelectJob: { jobType?: 'STUDENT' | 'EMPLOYEE' }; SelectSchool: { jobType: 'STUDENT'; school?: string }; SelectEmployee: { jobType: 'EMPLOYEE'; company?: string }; EnterJoinDate: { jobType: 'STUDENT'; school: string } | { jobType: 'EMPLOYEE'; company: string }; Confirm: ({ jobType: 'STUDENT'; school: string } | { jobType: 'EMPLOYEE'; company: string }) & { joinDate: string }; }>({ id: 'hello-world', initial: { step: 'SelectJob', context: {}, }, }); return ( history.push('SelectSchool', { jobType: 'STUDENT' }), selectEmployee: (_, { history }) => history.push('SelectEmployee', { jobType: 'EMPLOYEE' }), }, render({ dispatch }) { return ( dispatch('selectSchool')} onSelectEmployee={() => dispatch('selectEmployee')} /> ); }, })} SelectSchool={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, school, })) } /> )} SelectEmployee={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, company, })) } /> )} EnterJoinDate={funnel.Render.overlay({ render({ history, close }) { return ( history.push('Confirm', { joinDate })} onClose={() => close()} /> ); }, })} Confirm={({ context }) => context.jobType === 'STUDENT' ? ( ) : ( ) } /> ); } declare function SelectJob(props: { onSelectSchool(): void; onSelectEmployee(): void }): JSX.Element; declare function SelectSchool(props: { onNext(school: string): void }): JSX.Element; declare function SelectEmployee(props: { onNext(company: string): void }): JSX.Element; declare function EnterJoinDateBottomSheet(props: { onNext(joinDate: string): void; onClose(): void }): JSX.Element; declare function ConfirmStudent(props: { school: string; joinDate: string }): JSX.Element; declare function ConfirmEmployee(props: { company: string; joinDate: string }): JSX.Element; ``` -------------------------------- ### Funnel Render Overlay Example Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/funnel-render.mdx Demonstrates how to use FunnelRenderOverlay to render a TermsAgree step, passing context and handling navigation and closing. ```tsx history.push('PasswordInput', { email })} onClose={() => close()} /> ); } })} /> ``` -------------------------------- ### Comparing push() and replace() for Navigation Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/examples.mdx This example contrasts the behavior of history.push() and history.replace() in terms of how they modify the history stack and affect navigation. It clarifies when to use each method. ```tsx // Using push: A → B → C, back goes C → B → A history.push('B', contextB); history.push('C', contextC); // Using replace: A → B(replaced with C), back goes C → A history.push('B', contextB); history.replace('C', contextC); // B is overwritten ``` -------------------------------- ### Initialize and Use useFunnel Hook Source: https://context7.com/toss/use-funnel/llms.txt This example demonstrates initializing a sign-up funnel with different step contexts. The `useFunnel` hook automatically narrows the context type based on the current step, ensuring type safety. Use `funnel.history.push` to navigate to the next step with its associated context. ```tsx import { useFunnel } from '@use-funnel/browser'; // Step-by-step sign-up flow: each step requires progressively more fields. type EmailInput = { email?: string; password?: string; other?: unknown }; type PasswordInput = { email: string; password?: string; other?: unknown }; type OtherInput = { email: string; password: string; other?: unknown }; export function SignUpFunnel() { const funnel = useFunnel<{ EmailInput: EmailInput; PasswordInput: PasswordInput; OtherInput: OtherInput; }>({ id: 'sign-up', // unique key — used to namespace history state initial: { step: 'EmailInput', context: {}, }, }); // funnel.step — name of the active step (narrowed union) // funnel.context — context of the active step (type-narrowed automatically) // funnel.index — numeric index in history // funnel.historySteps — full history array switch (funnel.step) { case 'EmailInput': // funnel.context.email is `string | undefined` here return ( funnel.history.push('PasswordInput', { email })} /> ); case 'PasswordInput': // funnel.context.email is guaranteed `string` here return ( funnel.history.push('OtherInput', { password }) } /> ); case 'OtherInput': return ( funnel.history.push('OtherInput', { other }) } /> ); } } ``` -------------------------------- ### Configure Initial Step with useFunnel Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/get-started.mdx Initialize the useFunnel hook by specifying the funnel's ID, the overall state type, and the initial step with its context. This sets up the starting point for your multi-step flow. ```tsx import { useFunnel } from '@use-funnel/browser'; import type { EmailInput, PasswordInput, OtherInfoInput } from './context'; function MyFunnelApp() { const funnel = useFunnel<{ EmailInput: EmailInput; PasswordInput: PasswordInput; OtherInfoInput: OtherInfoInput; }>({ id: "my-funnel-app", initial: { step: "EmailInput", }, }); // ... ``` ```tsx import { useFunnel } from '@use-funnel/next'; import type { EmailInput, PasswordInput, OtherInfoInput } from './context'; function MyFunnelApp() { const funnel = useFunnel<{ EmailInput: EmailInput; PasswordInput: PasswordInput; OtherInfoInput: OtherInfoInput; }>({ id: 'my-funnel-app', initial: { step: "EmailInput", }, }); // ... ``` -------------------------------- ### Implementing UI Flows with useFunnel Hook Source: https://github.com/toss/use-funnel/blob/main/packages/react-router/README.md This example demonstrates how to use the `useFunnel` hook to manage a multi-step UI flow. It defines the types for each step, initializes the funnel with an ID and initial state, and renders different components based on the current step. Navigation between steps is handled using `history.push`, which can also update previous states. ```tsx import { useFunnel } from '@use-funnel/react-router'; export function App() { const funnel = useFunnel<{ SelectJob: { jobType?: 'STUDENT' | 'EMPLOYEE' }; SelectSchool: { jobType: 'STUDENT'; school?: string }; SelectEmployee: { jobType: 'EMPLOYEE'; company?: string }; EnterJoinDate: { jobType: 'STUDENT'; school: string } | { jobType: 'EMPLOYEE'; company: string }; Confirm: ({ jobType: 'STUDENT'; school: string } | { jobType: 'EMPLOYEE'; company: string }) & { joinDate: string }; }>({ id: 'hello-world', initial: { step: 'SelectJob', context: {}, }, }); return ( history.push('SelectSchool', { jobType: 'STUDENT' }), selectEmployee: (_, { history }) => history.push('SelectEmployee', { jobType: 'EMPLOYEE' }), }, render({ dispatch }) { return ( dispatch('selectSchool')} onSelectEmployee={() => dispatch('selectEmployee')} /> ); }, })} SelectSchool={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, school, })) } /> )} SelectEmployee={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, company, })) } /> )} EnterJoinDate={funnel.Render.overlay({ render({ history, close }) { return ( history.push('Confirm', { joinDate })} onClose={() => close()} /> ); }, })} Confirm={({ context }) => context.jobType === 'STUDENT' ? ( ) : ( ) } /> ); } declare function SelectJob(props: { onSelectSchool(): void; onSelectEmployee(): void }): JSX.Element; declare function SelectSchool(props: { onNext(school: string): void }): JSX.Element; declare function SelectEmployee(props: { onNext(company: string): void }): JSX.Element; declare function EnterJoinDateBottomSheet(props: { onNext(joinDate: string): void; onClose(): void }): JSX.Element; declare function ConfirmStudent(props: { school: string; joinDate: string }): JSX.Element; declare function ConfirmEmployee(props: { company: string; joinDate: string }): JSX.Element; ``` -------------------------------- ### Configure Typed Funnel Steps with useFunnel Source: https://context7.com/toss/use-funnel/llms.txt Use `createFunnelSteps` to build type-safe steps that progressively enforce required fields. The `initial` option specifies the starting step and context. ```tsx import { useFunnel, createFunnelSteps } from '@use-funnel/next'; type FormState = { email?: string; password?: string; nickname?: string; }; // Build typed steps that progressively mark fields as required. const steps = createFunnelSteps() .extends('EmailInput') // email optional .extends('PasswordInput', { requiredKeys: 'email' }) // email required .extends('NicknameInput', { requiredKeys: 'password' }) // password required .build(); export function App() { const funnel = useFunnel({ id: 'registration', initial: { step: 'EmailInput', context: {} }, steps, // attaches guard functions auto-generated by createFunnelSteps }); // At 'PasswordInput': funnel.context.email is `string` (not string | undefined) // At 'NicknameInput': funnel.context.password is `string` return ( history.push('PasswordInput', { email: e.target.value })} /> )} PasswordInput={({ context, history }) => ( history.push('NicknameInput', { password: e.target.value })} /> )} NicknameInput={({ context }) => (

Welcome, {context.email}! Enter your nickname.

)} />; } ``` -------------------------------- ### Clone the Repository Source: https://github.com/toss/use-funnel/blob/main/CONTRIBUTING.md Clone the use-funnel repository to your local machine. Ensure you have Git installed. ```shell git clone git@github.com:{username}/use-funnel.git ``` -------------------------------- ### UseFunnelOptions Interface Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/use-funnel.mdx Defines the funnel's initial state and configuration for each step. The `id` is required for identification, and `initial` sets the starting point. `steps` allows custom logic per step. ```typescript interface UseFunnelOptions { id: string; initial: { step: keyof T, context: T[keyof T] }; steps?: { [key in keyof T]?: FunnelStepOption }; } ``` -------------------------------- ### Main Funnel with Nested Sub-Funnels Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/sub-funnel.mdx This example demonstrates how to define a main funnel that incorporates a nested sub-funnel. The `useFunnel` hook is used with a specific `id` for the main funnel, and a component (`BFunnel`) is rendered which itself uses `useFunnel` with a different `id` for the sub-funnel. This structure allows for modular funnel design. ```tsx import { useFunnel } from '@use-funnel/react-router-dom'; import { BFunnel } from './BFunnel'; export function Example() { const funnel = useFunnel({ id: 'main-funnel', initial: { step: 'A', }, }); return ( (

A Step

)} B={({ context, history }) => (

B Step

history.push('C', { b })} />
)} C={({ context }) => (

C Step

a: {context.a}

b: {context.b}

)} /> ); } ``` ```tsx import { useFunnel } from '@use-funnel/react-router-dom'; interface Props { a: string; onNext: (b: string) => void; } export function BFunnel({ a, onNext }: Props) { const funnel = useFunnel({ id: 'b-funnel', initial: { step: 'B1', }, }); return ( (

B1 Step

previous a value: {a}

)} B2={({ context, history }) => (

B2 Step

previous a value: {a}

previous hello value: {context.hello}

)} B3={({ context }) => (

B3 Step

previous a value: {a}

previous hello value: {context.hello}

previous world value: {context.world}

)} /> ); } ``` -------------------------------- ### Funnel Navigation With Value Preservation using replace Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/examples.mdx This example demonstrates how to preserve input values on back navigation by using history.replace() to update the current step's context before navigating forward. This ensures that values are available when navigating back. ```tsx function MyFunnel() { const funnel = useFunnel({ id: 'sign-up', initial: { step: 'EmailInput', context: {} }, }); return ( ( history.replace('EmailInput', { email })} onNext={(email) => history.push('PasswordInput', { email })} /> )} PasswordInput={({ context, history }) => ( history.push('Done', { ...context, password })} onBack={() => history.back()} /> )} Done={({ context }) => (

Signed up as {context.email}

)} /> ); } ``` -------------------------------- ### Define and Use Transition Events in a Funnel Step Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/transition-event.mdx This example demonstrates defining `EmailInputSuccess` and `EmailInputFail` events for an email input step. The `EmailInputSuccess` event transitions to the `PasswordInput` step, while `EmailInputFail` transitions to an `ErrorPage`. Use `dispatch()` to trigger these events from your component's event handlers. ```tsx import { useFunnel } from "@use-funnel/next"; const funnel = useFunnel(/* ... */); { // 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 ( dispatch('EmailInputSuccess', email)} // Dispatch EmailInputFail event when email input fails onError={(error) => dispatch('EmailInputFail', error)} /> ); } })} /> ``` -------------------------------- ### Basic useFunnel Initialization Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/use-funnel.mdx Demonstrates the basic initialization of the useFunnel hook with a defined type for context and initial step configuration. ```typescript import { useFunnel, UseFunnelOptions, UseFunnelResults } from "@use-funnel/next"; type T = { helloStep: { message: string; }; worldStep: { message: string; message2: string } } const funnel: UseFunnelResults = useFunnel({ id: "use-funnel-api-reference", steps: {}, initial: { step: "helloStep", context: { message: 'Hello' } }, } satisfies UseFunnelOptions); ``` -------------------------------- ### Basic Usage of useFunnel Hook Source: https://github.com/toss/use-funnel/blob/main/packages/react-router-dom/README.md Demonstrates how to initialize and use the useFunnel hook with typed steps and event handlers for navigation. Ensure all declared steps and their associated contexts are correctly defined. ```tsx import { useFunnel } from '@use-funnel/react-router-dom'; export function App() { const funnel = useFunnel({ id: 'hello-world', initial: { step: 'SelectJob', context: {}, }, }); return ( history.push('SelectSchool', { jobType: 'STUDENT' }), selectEmployee: (_, { history }) => history.push('SelectEmployee', { jobType: 'EMPLOYEE' }), }, render({ dispatch }) { return ( dispatch('selectSchool')} onSelectEmployee={() => dispatch('selectEmployee')} /> ); }, })} SelectSchool={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, school, })) } /> )} SelectEmployee={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, company, })) } /> )} EnterJoinDate={funnel.Render.overlay({ render({ history, close }) { return ( history.push('Confirm', { joinDate })} onClose={() => close()} /> ); }, })} Confirm={({ context }) => context.jobType === 'STUDENT' ? ( ) : ( ) } /> ); } declare function SelectJob(props: { onSelectSchool(): void; onSelectEmployee(): void }): JSX.Element; declare function SelectSchool(props: { onNext(school: string): void }): JSX.Element; declare function SelectEmployee(props: { onNext(company: string): void }): JSX.Element; declare function EnterJoinDateBottomSheet(props: { onNext(joinDate: string): void; onClose(): void }): JSX.Element; declare function ConfirmStudent(props: { school: string; joinDate: string }): JSX.Element; declare function ConfirmEmployee(props: { company: string; joinDate: string }): JSX.Element; ``` -------------------------------- ### Implement a Multi-Step Flow with useFunnel Source: https://github.com/toss/use-funnel/blob/main/packages/next/README.md Use the `useFunnel` hook to manage a multi-step user flow with type-safe state transitions. Define steps with their expected context and render components for each step, handling navigation and state updates. ```tsx import { useFunnel } from '@use-funnel/next'; export function App() { const funnel = useFunnel({ id: 'hello-world', initial: { step: 'SelectJob', context: {}, }, }); return ( history.push('SelectSchool', { jobType: 'STUDENT' }), selectEmployee: (_, { history }) => history.push('SelectEmployee', { jobType: 'EMPLOYEE' }), }, render({ dispatch }) { return ( dispatch('selectSchool')} onSelectEmployee={() => dispatch('selectEmployee')} /> ); }, })} SelectSchool={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, school, })) } /> )} SelectEmployee={({ history }) => ( history.push('EnterJoinDate', (prev) => ({ ...prev, company, })) } /> )} EnterJoinDate={funnel.Render.overlay({ render({ history, close }) { return ( history.push('Confirm', { joinDate })} onClose={() => close()} /> ); }, })} Confirm={({ context }) => context.jobType === 'STUDENT' ? ( ) : ( ) } /> ); } declare function SelectJob(props: { onSelectSchool(): void; onSelectEmployee(): void }): JSX.Element; declare function SelectSchool(props: { onNext(school: string): void }): JSX.Element; declare function SelectEmployee(props: { onNext(company: string): void }): JSX.Element; declare function EnterJoinDateBottomSheet(props: { onNext(joinDate: string): void; onClose(): void }): JSX.Element; declare function ConfirmStudent(props: { school: string; joinDate: string }): JSX.Element; declare function ConfirmEmployee(props: { company: string; joinDate: string }): JSX.Element; ``` -------------------------------- ### Client-Side Rendering for Browser Funnel Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/get-started.mdx When using @use-funnel/browser, ensure the component runs exclusively on the client due to its reliance on browser history state. This example shows how to conditionally render the funnel component after the component mounts. ```tsx import { useState, useEffect } from 'react'; export default function Page() { const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); if (!isMounted) return null; return ; } ``` -------------------------------- ### Define Typed Steps with useFunnel Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/features.mdx Shows how to define typed steps and their contexts using the `useFunnel` hook. This ensures type safety during state transitions and provides clear context requirements for each step. ```typescript const funnel = useFunnel<{ A: { a?: string; b?: string }; B: { a: string; b?: string }; }>({ id: "strongly-typed", initial: { step: "A", } }); // When the initial step is "A", the type of context.a is "string" or "undefined" funnel.step === "A" && typeof funnel.context.a // "string" | "undefined" // When the step changes to "B", the type of context.a is "string" funnel.step === "B" && typeof funnel.context.a // "string" ``` -------------------------------- ### Create a Self-Contained Funnel with useState Source: https://context7.com/toss/use-funnel/llms.txt This example demonstrates creating a fully self-contained funnel using React's `useState` hook for history management. It's useful for in-modal flows or testing scenarios where a router dependency is not desired. ```tsx import { useMemo, useState } from 'react'; import { createUseFunnel } from '@use-funnel/core'; // A fully self-contained funnel that stores history in React state // (no router dependency — useful for in-modal flows or testing). export const useFunnel = createUseFunnel(({ id, initialState }) => { const [history, setHistory] = useState(() => [initialState]); const [currentIndex, setCurrentIndex] = useState(0); return useMemo( () => ({ history, currentIndex, push(state) { setHistory((prev) => [...prev.slice(0, currentIndex + 1), state]); setCurrentIndex((i) => i + 1); }, replace(state) { setHistory((prev) => [...prev.slice(0, currentIndex), state]); }, go(delta) { setCurrentIndex((i) => Math.max(0, Math.min(history.length - 1, i + delta))); }, cleanup() { // reset state when component unmounts (optional) }, }), [history, currentIndex], ); }); // Usage is identical to any other @use-funnel adapter: function InModalWizard() { const funnel = useFunnel({ id: 'modal-wizard', initial: { step: 'Step1' }, }); return ( ( history.push('Step2', { name: e.target.value })} /> )} Step2={({ context }) =>

Hello, {context.name}!

} /> ); } ``` -------------------------------- ### Render Steps as Overlays with `funnel.Render.overlay()` Source: https://context7.com/toss/use-funnel/llms.txt Employ `funnel.Render.overlay()` to display a step as an overlay while keeping the previous step visible. The `close()` function is crucial for navigating back when the overlay is dismissed without advancing the funnel. ```tsx import { useFunnel } from '@use-funnel/react-router-dom'; const funnel = useFunnel<{ SchoolInput: { school?: string }; StartDateInput: { school: string; startDate?: string }; Complete: { school: string; startDate: string }; }>({ id: 'enrollment', initial: { step: 'SchoolInput' } }); return ( ( history.push('StartDateInput', { school })} /> )} StartDateInput={funnel.Render.overlay({ render({ history, close }) { return ( history.push('Complete', { startDate }) } onDismiss={() => close()} // must call close() on non-router dismissal /> ); }, })} Complete={({ context }) => (

{context.school} — starting {context.startDate}

)} /> ); ``` -------------------------------- ### Add a New Router Package Source: https://github.com/toss/use-funnel/blob/main/CONTRIBUTING.md Use the provided pnpm command to add a new router package to the project. Replace 'your-router-name' with the desired name. ```shell pnpm add:router --name your-router-name ``` -------------------------------- ### useFunnel Hook Configuration Source: https://context7.com/toss/use-funnel/llms.txt The `useFunnel` hook accepts a configuration object with `id`, `initial` state, and optional `steps` for validation. The `steps` can be built using `createFunnelSteps` for type safety and progressive field requirements. ```APIDOC ## `useFunnel` Hook ### Configuration Object (`UseFunnelOptions`) - **id** (string) - Required - A unique identifier for the funnel. - **initial** (object) - Required - The initial state of the funnel. - **step** (string) - Required - The name of the initial step. - **context** (object) - Optional - Initial context data for the funnel. - **steps** (object) - Optional - Configuration for step validation, built using `createFunnelSteps`. ``` -------------------------------- ### Sequence Diagram: State Transition A to B Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/features.mdx Illustrates a state transition from step A to step B, showing the state update. ```mermaid sequenceDiagram A->>B: state: { A: true } B->>C: state: { A: true, B: false } ``` -------------------------------- ### Build Type-Safe Steps with `createFunnelSteps()` Source: https://context7.com/toss/use-funnel/llms.txt Utilize `createFunnelSteps()` to construct a type-safe step map by chaining `.extends()` calls, progressively enforcing context keys. This map is then passed to the `steps` option of `useFunnel()`. ```tsx import { createFunnelSteps, useFunnel } from '@use-funnel/browser'; type CheckoutState = { cartItems?: string[]; address?: string; paymentMethod?: string; }; // Build a type-safe step map: each step enforces previously collected fields. const checkoutSteps = createFunnelSteps() .extends('CartReview') // all optional .extends('AddressInput', { requiredKeys: 'cartItems' }) // cartItems required .extends('PaymentInput', { requiredKeys: 'address' }) // address required .extends('OrderConfirm', { requiredKeys: 'paymentMethod' }) // paymentMethod required .build(); export function CheckoutFlow() { const funnel = useFunnel({ id: 'checkout', initial: { step: 'CartReview', context: {} }, steps: checkoutSteps, }); return ( ( history.push('AddressInput', { cartItems: items })} /> )} AddressInput={({ context, history }) => ( // context.cartItems: string[] (guaranteed — not string[] | undefined) history.push('PaymentInput', { address })} /> )} PaymentInput={({ history }) => ( history.push('OrderConfirm', { paymentMethod: method })} /> )} OrderConfirm={({ context }) => (

Confirm order to {context.address} via {context.paymentMethod}

)} /> ); } ``` -------------------------------- ### Render Steps with Transition Events using `funnel.Render.with()` Source: https://context7.com/toss/use-funnel/llms.txt Use `funnel.Render.with()` to separate navigation logic from UI rendering. Event handlers receive payload and context, allowing navigation via `history.push()`. Ensure `close()` is called on non-router dismissals. ```tsx import { useFunnel } from '@use-funnel/next'; const funnel = useFunnel<{ EmailInput: { email?: string }; PasswordInput: { email: string }; ErrorPage: { error: string }; }>({ id: 'login-events', initial: { step: 'EmailInput' } }); return ( history.push('PasswordInput', { email }), EmailInputFail: (error: Error, { history }) => history.push('ErrorPage', { error: error.message }), }, render({ context, dispatch }) { return ( dispatch('EmailInputSuccess', email)} onError={(err) => dispatch('EmailInputFail', err)} /> ); }, })} PasswordInput={({ context }) =>

Hello {context.email}

} ErrorPage={({ context }) =>

Error: {context.error}

} /> ); ``` -------------------------------- ### Migrate `withState()` to Generics Source: https://github.com/toss/use-funnel/blob/main/docs/src/pages/en/docs/migration.mdx Replace `withState()` with generics in `createFunnelSteps()` for defining step-specific state. ```tsx import { useFunnel } from '@toss/use-funnel'; const [Funnel, state, setState] = useFunnel([ 'A', 'B', 'C' ] as const).withState<{ foo?: string; bar?: number; }>({ foo: 'Hello', bar: 5 }); ``` ```tsx import { useFunnel, createFunnelSteps } from '@use-funnel/next'; const steps = createFunnelSteps<{ foo?: string; bar?: number; }>() .extends(['A', 'B', 'C']) .build(); const funnel = useFunnel({ steps, initial: { step: 'A', context: { foo: 'Hello', bar: 5, }, }, }); ``` -------------------------------- ### createFunnelSteps() — Step Builder Utility Source: https://context7.com/toss/use-funnel/llms.txt `createFunnelSteps()` is a utility for building per-step validation options by chaining `.extends()` calls. This progressively makes optional context keys required, and the result is passed to the `steps` option of `useFunnel()`. ```APIDOC ## `createFunnelSteps()` — Step Builder Utility `createFunnelSteps()` builds per-step validation options by chaining `.extends()` calls, progressively making optional context keys required. The result is passed to the `steps` option of `useFunnel()`. ```tsx import { createFunnelSteps, useFunnel } from '@use-funnel/browser'; type CheckoutState = { cartItems?: string[]; address?: string; paymentMethod?: string; }; // Build a type-safe step map: each step enforces previously collected fields. const checkoutSteps = createFunnelSteps() .extends('CartReview') // all optional .extends('AddressInput', { requiredKeys: 'cartItems' }) // cartItems required .extends('PaymentInput', { requiredKeys: 'address' }) // address required .extends('OrderConfirm', { requiredKeys: 'paymentMethod' }) // paymentMethod required .build(); export function CheckoutFlow() { const funnel = useFunnel({ id: 'checkout', initial: { step: 'CartReview', context: {} }, steps: checkoutSteps, }); return ( ( history.push('AddressInput', { cartItems: items })} /> )} AddressInput={({ context, history }) => ( // context.cartItems: string[] (guaranteed — not string[] | undefined) history.push('PaymentInput', { address })} /> )} PaymentInput={({ history }) => ( history.push('OrderConfirm', { paymentMethod: method })} /> )} OrderConfirm={({ context }) => (

Confirm order to {context.address} via {context.paymentMethod}

)} /> ); } ``` ```