react-call

repository·main·Indexed 23 days ago

https://github.com/desko27/react-call

A library that allows developers to treat React components as asynchronous functions. It enables 'calling' UI components—such as dialogs, toasts, or modals—and awaiting their response using a Promise-based API. Key features include createCallable for defining callable entities, a Root component for mounting, upsert() for singleton UI, and useMutationFlow for managing async actions. It supports exit animations, HMR via a Vite plugin, and provides a mount() helper for multi-preview hosts like Storybook.

Tokens
28.5K
Snippets
45
Records
146
Agent score
78%

What's inside react-call

  1. How to manage multiple concurrent banners using .call()

    main

    Instead of manually managing an array of banners in your component state (e.g., useState<Banner[]>([])), use ErrorBanner.call(...).

    Each call to .call() creates an independent entry in the Root's Stack. The library manages the list for you. Because each call is a unique entry, they coexist concurrently. You can use the call.index property provided within the Callable to calculate vertical offsets or positioning, as call.index represents the specific call's position in the current Stack.

  2. How react-call works: The Declare → Root → Call model

    main

    The react-call library allows you to turn a React component into something you can await imperatively. The workflow follows three distinct steps:

    1. Declare: Use createCallable<Props, Response, RootProps>() to define your component. The component receives a special call prop (the CallContext) which provides methods to resolve the interaction.
    2. Root: Mount the resulting Callable component (the Root) exactly once in a high-level part of your application (e.g., App.tsx or a layout) so it is always available to receive calls.
    3. Call & await: Invoke the component's namespace methods (like .call()) from anywhere in your async code to trigger the UI and await its result.

    Generics for createCallable:

    • Props: The props passed to each individual call.
    • Response: The type of value the promise resolves to.
    • RootProps (optional): Props passed to the Root component itself for data shared across all calls.
    import { createCallable } from 'react-call'
    
    interface Props { message: string }
    type Response = boolean
    
    // 1. Declare
    export const Confirm = createCallable<Props, Response>(({ call, message }) => (
      <div role="dialog">
        <p>{message}</p>
        <button onClick={() => call.end(true)}>Yes</button>
        <button onClick={() => call.end(false)}>No</button>
      </div>
    ))
    
    // 2. Root (Mount once in App.tsx)
    // <Confirm />
    
    // 3. Call & await
    const accepted = await Confirm.call({ message: 'Continue?' })
  3. Choose between `Toast.upsert` and `Toast.call`

    main

    Decide which method to use based on whether you want to update a single UI element or stack multiple independent notifications:

    • Toast.upsert(props): Use this for singletons. It updates the existing toast instance. Ideal for progress bars or status indicators where you want one persistent element that evolves.
    • Toast.call(props): Use this when each event deserves its own toast. This method causes the Root to stack toasts on top of each other (e.g., for error banners or discrete notifications).
  4. Using .call(props) to bridge caller-side context

    main

    The .call(props) method acts as the seam where caller-side context—such as cursor position, specific row data from a right-click, or other event-driven data—is transferred into a Callable.

    When an event occurs in your component, .call() snapshots the necessary data and opens the Callable with that state. This pattern is ideal for:

    • Right-click menus: Passing x and y coordinates.
    • Popovers: Passing anchor coordinates or element references.
    • Pickers: Passing the trigger's position or associated data.
  5. How `react-call/vite` handles `displayName` injection

    main

    The react-call/vite plugin works by walking each TS/TSX/JS/JSX module during Vite's development cycle.

    Implementation Details

    • Detection: It looks for top-level exports using createCallable (even if createCallable is renamed via import).
    • Injection Timing: The plugin runs with enforce: 'pre', meaning it sees your source code before Vite's esbuild type-stripping or @vitejs/plugin-react's Fast Refresh transformations. The injection is performed as a string append at the end of the module.
    • Source Maps: The plugin returns { code, map: null } for the injection. This instructs Vite to preserve the existing source maps for your original code, while the injected lines (which only exist in dev) do not affect your original source positions or stack traces.
    • Safety: It uses a "skip-on-manual" strategy. If it detects you have already set a displayName manually, it will not inject one, preventing the plugin from overriding your explicit intent.
  6. Deciding between Per-call or Root props

    main

    To decide which prop type to use, evaluate the nature of the data:

    • Use Per-call props if the value is specific to a single invocation (e.g., a specific message, a specific item being acted upon, or a unique ID).
    • Use Root props if the value is ambient (e.g., the same for every call and owned by the surrounding application context).
  7. Core Concepts of react-call

    main

    Understanding the fundamental building blocks of the library:

    • Callable: The primary unit created via createCallable(). It serves two roles: a React component (e.g., <Confirm />) used to mount the Root, and a namespace for imperative methods like call, upsert, end, and update.
    • Root: The component instance of a Callable mounted within the React tree. This is the mounting point for the UI.
    • Call: A single imperative invocation of a Callable (e.g., Confirm.call({...})). Each call is independent and can run concurrently with others.
    • Stack: The ordered list of all currently active Calls for a specific Callable. The Root renders this stack, with newer calls appearing later in the iteration order.
    • CallContext: The specific props received by a user component for a single active call. It provides access to end, ended, key, index, stackSize, and root.
    • Upsert: A singleton-style operation. The first upsert() creates the call, and subsequent upsert() calls update the props of the existing call. It returns the same promise across the singleton's lifetime.
    • Caller scope: The location (typically a feature component or domain handler) where Callable.call() is invoked and where the resulting async logic and response handling reside.
  8. Understand the test coverage ratchet gate

    main

    The project uses a "ratchet" mechanism for test coverage rather than fixed aspirational targets. This means coverage thresholds are set to the current measured levels and are only allowed to move up, never down. This prevents coverage regressions without forcing developers to write tests for unrelated changes just to pass CI.

    Key characteristics of the coverage gate:

    • Tooling: Uses @vitest/coverage-v8 configured in the root vitest.config.ts.
    • Scope: Uses all: true to ensure the percentage describes the entire packages/*/src runtime tree, including files that are not explicitly imported by any test. This prevents untested modules from being hidden by a high average.
    • CI Integration: Coverage is run by executing pnpm test:coverage, which folds the coverage check into the existing test CI job. A threshold miss fails the job.
    • Threshold Behavior: Thresholds are floored integers to provide a small amount of headroom for routine refactors that might slightly shift statement or line counts without affecting actual logic coverage.
  9. Choosing between .call() and .upsert() for notifications

    main

    Deciding how to handle UI updates depends on whether you want multiple instances to coexist or a single instance to update in place:

    • Use .call() when concurrent instances should coexist. This is ideal for error toasts, per-item notifications, or any scenario where a second event should not clobber (replace) the first.
    • Use .upsert() when you want a single surface that updates in place. This is ideal for progress bars or status indicators (e.g., changing a pill from "saving..." to "saved").

    The deciding question: Should a new event add a new banner to the stack, or update the existing one?

  10. Understanding react-call distribution tags (latest, next, dev)

    main

    The react-call package uses npm distribution tags to manage different release channels. As of the v2 stable transition, the following behavior applies to these tags:

    • latest: Points to the current stable version (e.g., 2.0.0). This is the default version installed when running npm install react-call.
    • next: Historically used for prereleases, it is now synchronized with latest (i.e., next == latest) once a stable version is released. This ensures that users who explicitly install using npm install react-call@next receive the stable version rather than an outdated prerelease.
    • dev: This tag has been removed as it pointed to legacy/orphaned artifacts (like 1.9.0-dev.0).

    Important Note for Consumers: It is highly discouraged to pin your package.json to a literal tag string (e.g., "react-call": "next"). Instead, pin to a specific version number to ensure stability and avoid unexpected updates when tags are moved.

  11. Targeted vs. Broadcast updates

    main

    When using update, choose between two modes based on your requirements:

    1. Targeted Update: update(promise, props). This pushes updates to one specific call that the caller is currently tracking via a promise. Use this when each call has its own independent lifecycle.
    2. Broadcast Update: update(props). This pushes updates to all currently open calls. Use this for global or ambient state that all active calls must reflect (e.g., connectivity changes or a shared clock).
  12. Test environment and architecture

    main

    The react-call test suite is colocated within the library package at packages/react-call/src/__tests__/. This architecture allows for a fast development loop because Vitest watches the source files directly, removing the need to run a build step (pnpm build) before running tests.

    Key Technologies:

    • Runner: Vitest
    • DOM Emulation: happy-dom (provides a fast, lightweight alternative to headless Chromium)
    • Testing Library: @testing-library/react for rendering/queries, @testing-library/user-event for interactions, and @testing-library/jest-dom for DOM matchers.

    Important Constraints:

    • Fidelity: happy-dom emulates the DOM and Event constructors but is not a full browser. It may diverge from Chromium regarding CSS layout, computed styles, and specific microtask/event-loop orderings.
    • Visual Validation: For visual or CSS-driven regressions, refer to the live demo at react-call.desko.dev, which uses a real browser.