Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
R

react-call

> 前端框架
Open source

Call & Await React Components

1.3K stars0 likes0 views
WebsiteGitHub

About

Call & Await React Components

`createCallable()` turns a React component into something you can `await`.

Good fits: confirmations, dialogs, form modals, toasts, notifications, context menus, pickers — any UI that conceptually returns a value to its caller. ## Contents - [Getting started](#getting-started) - [1. ⚛️ Declare](#1-️-declare) - [2. Root](#2--root) - [3. ▶️ Call \& Await](#3-️-call--await) - [Advanced usage](#advanced-usage) - [End from caller](#end-from-caller) - [Update](#update) - [Upsert](#upsert) - [Exit animations](#exit-animations) - [Passing Root props](#passing-root-props) - [Mutation flow](#mutation-flow) - [Optional mutationFn](#optional-mutationfn) - [Payload](#payload) - [Hot reload (HMR)](#hot-reload-hmr) - [Vite plugin (optional)](#vite-plugin-optional) - [Multi-preview hosts (Storybook, Ladle, …)](#multi-preview-hosts-storybook-ladle-) - [With static providers](#with-static-providers) - [With reactive providers](#with-reactive-providers) - [Options](#options) - [FAQ](#faq) - [What if more than one call is active?](#what-if-more-than-one-call-is-active) - [Can I place more than one Root?](#can-i-place-more-than-one-root) - [TypeScript types](#typescript-types) - [Errors](#errors) - [Lazy loading](#lazy-loading) - [SSR](#ssr) - [Next.js / RSC](#nextjs--rsc) - [AI agent skill](#ai-agent-skill) - [Migrating from v1](#migrating-from-v1) # Getting started > [!NOTE] > These docs cover **v2**, the current stable release. Upgrading from 1.x? See [Migrating from v1](#migrating-from-v1) — or the [v1 README](https://github.com/desko27/react-call/blob/react-call%401.8.2/README.md) for the old API. ```sh npm install react-call ``` We'll setup a confirmation dialog, but you can setup any component to be callable. ## 1. ⚛️ Declare ```tsx import { createCallable } from 'react-call' interface Props { message: string } type Response = boolean export const Confirm = createCallable(({ call, message }) => ( )) ``` Along with your props, there is a special `call` prop containing the `end()` method, which you can use to finish the call and return a response. State, hooks and any other React features are totally fine too. ## 2. Root The Callable itself is the mounting point — it listens to every call and renders the active ones. Place it anywhere visible when making calls, for instance in `App.tsx`: ```diff + // ^-- it will render active calls ``` ## 3. ▶️ Call & Await You're all done! Now you can do this anywhere in your codebase: ```tsx // ↙ response props ↘ const accepted = await Confirm.call({ message: 'Continue?' }) ``` Want to see more? The [**examples gallery**](https://react-call.desko.dev/examples) has live demos of confirm dialogs, command palettes, toasts, multi-step wizards, drawers and more — each with its source and an **Open in CodeSandbox** button. # Advanced usage ## End from caller The returned promise can be used to end the call from the caller scope: ```tsx const promise = Confirm.call({ message: 'Continue?' }) // For example, on some event subscription onImportantEvent(() => { Confirm.end(promise, false) }) // And still await the response where needed const accepted = await promise ``` While the promise argument is used to target that specific call, all ongoing calls can be affected by omitting it: ```tsx // All confirm calls are ended with `false` Confirm.end(false) ``` ## Update The returned promise can also be used to update the call props on the fly: ```tsx const promise = Alert.call({ message: 'Starting operation...' }) await asyncOperation() Alert.update(promise, { message: 'Completed!' }) ``` While the promise argument is used to target that specific call, all ongoing calls can be affected by omitting it: ```tsx // All alert calls are updated with the new message prop Alert.update({ message: 'Completed!' }) ``` ## Upsert If you need to ensure only one instance of a component is active at a time, use `upsert()` instead of `call()`. This is particularly useful for notifications, loading states, or any singleton-like UI: ```tsx // First call creates a new instance const promise1 = Toast.upsert({ message: 'Loading...' }) // Second call updates the existing instance instead of creating a new one const promise2 = Toast.upsert({ message: 'Almost done...' }) // promise1 === promise2 (same instance) console.log(promise1 === promise2) // true ``` The `upsert()` method behaves as follows: - Creates a new instance if no upsert instance is currently active - Updates the existing upsert instance if one is already active - Does not affect normal `call()` instances - Creates a new instance if the previous upsert instance was ended ```tsx // Example: progress notification that updates itself const showProgress = async () => { Toast.upsert({ message: 'Starting download...' }) for (let i = 0; i <= 100; i += 10) { await new Promise(resolve => setTimeout(resolve, 100)) Toast.upsert({ message: `Progress: ${i}%` }) } Toast.end() } ``` # Exit animations To animate the exit of your component when `call.end()` is run, just pass the duration of your animation in milliseconds to createCallable as a second argument: ```diff + const UNMOUNTING_DELAY = 500 export const Confirm = createCallable( ({ call }) => ( ) }, ) await Confirm.call({ mutationFn: async (call) => { await api.delete(id) call.end(true) }, }) ``` The `mutationFn` receives the call context and decides when — if ever — to close. ## Optional mutationFn If a caller may omit `mutationFn`, type the prop as optional and chain `.orEnd(value)` at the callsite. The chain fires only when no `mutationFn` was provided; with one, it's a no-op. ```tsx type Props = { mutationFn?: MutationFn } export const Confirm = createCallable(({ call, mutationFn }) => { const submit = useMutationFlow(call, mutationFn) return ( // closes with `true` if no mutationFn ↓ submit().orEnd(true)}>Yes ) }) ``` ## Payload `MutationFn` is ``-shaped. `Payload` is the second generic and defaults to `void`, so `submit()` takes no argument unless you opt in. ```tsx type Props = { mutationFn: MutationFn } // ↑ payload type export const Create = createCallable(({ call, mutationFn }) => { const [name, setName] = useState('') const submit = useMutationFlow(call, mutationFn) return ( ) }) await Create.call({ mutationFn: async (call, payload) => { // ↑ typed as { name: string } await api.create(payload.name) call.end(true) }, }) ``` The payload is typed end-to-end — the trigger callsite and the handler share the same `Payload` generic — and it lives at the callsite, so triggers in the same component can forward different payloads (useful for pickers, where each option carries its own data). # Hot reload (HMR) `createCallable` is Fast Refresh friendly — edits to your callable's source hot-update in place without a full page reload. If you want the **open dialog to survive across saves** of its own source, set a `displayName` on the callable: ```diff export const Confirm = createCallable(({ call, message }) => ( )) + Confirm.displayName = 'Confirm' ``` Callables without a `displayName` still HMR — only the dialog you're editing resets; sibling state in the rest of the page is preserved either way. ## Vite plugin (optional) If you're on Vite, the bundled plugin auto-injects the `displayName` line so you don't have to write it: ```ts // vite.config.ts import react from '@vitejs/plugin-react' import reactCall from 'react-call/vite' export default { plugins: [react(), reactCall()], } ``` With the plugin enabled, every top-level `(export) const X = createCallable(...)` gets `X.displayName = 'X'` appended at dev time only — no source change, no production overhead. # Multi-preview hosts (Storybook, Ladle, …) Tools like Storybook (autodocs page), Ladle, Histoire, and react-cosmos render multiple stories side-by-side. If each story's decorator mounts ``, every preview registers its own listener — and `Confirm.call()` throws `Multiple instances of found!` the moment any preview's button is clicked. `react-call/host` exposes a `mount()` helper that puts a single Root in a body-level `` outside the previews. Call it once from your host's preview entry file (e.g. `.storybook/preview.tsx`); your story decorators don't need to render Callables at all. ```tsx // .storybook/preview.tsx import { mount } from 'react-call/host' import { Confirm } from '../src/Confirm' mount() const preview = { /* normal Storybook config */ } export default preview ``` That's it for the simple case. Your app's own `` mount stays where it is — this helper only handles the preview environment. If you were previously rendering `` from inside a story decorator, drop it from the decorator. The mount is idempotent under HMR — saving your `preview.tsx` doesn't double-mount, and an open `Confirm.call()` survives the edit. ## With static providers The Confirm renders in its own React tree, separate from every story preview. It does not inherit context from your story decorators — if it needs a theme, locale, or router, pass them via `wrapper`: ```tsx import { mount } from 'react-call/host' import { ThemeProvider } from '@mui/material/styles' import { lightTheme } from '../src/themes' import { Confirm } from '../src/Confirm' mount(, { wrapper: ({ children }) => ( {children} ), }) ``` ## With reactive providers A static wrapper captures its props once. If your providers depend on Storybook globals — toolbar toggles, args, parameters — subscribe to them inside the wrapper via `useGlobals` from `@storybook/preview-api`: ```tsx import type { ReactNode } from 'react' import { mount } from 'react-call/host' import { useGlobals } from '@storybook/preview-api' import { ThemeProvider } from '@mui/material/styles' import { lightTheme, darkTheme } from '../src/themes' import { Confirm } from '../src/Confirm' function ReactiveTheme({ children }: { children: ReactNode }) { const [{ theme = 'light' }] = useGlobals() return ( {children} ) } mount(, { wrapper: ReactiveTheme }) ``` External stores (Zustand, Jotai, Redux, anything backed by `useSyncExternalStore`) work the same way — both trees subscribe to the same source of truth. ## Options ```tsx mount(element, { wrapper?: ComponentType<{ children: ReactNode }>, container?: HTMLElement, // default: in document.body }) ``` Works wherever React DOM does. # FAQ ### What if more than one call is active? `` works as a call stack. Multiple calls will render one after another (newer below, which is one on top of the other if your CSS is position fixed/absolute). ### Can I place more than one Root? No. There can only be one `` mounted per createCallable(). Avoid placing it in multiple locations of the React Tree loaded at once, an error will be thrown if so. If you specifically need this in a sandbox host (Storybook autodocs, Ladle, …), see [Multi-preview hosts](#multi-preview-hosts-storybook-ladle-) for the supported pattern. # TypeScript types You won't need them most likely, but if you want to split the component declaration and such, the public types are available as named exports: ```tsx import type { UserComponent, CallContext } from 'react-call' ``` Type | Description --- | --- CallFunction | T

GitHub Issues· 0 open

View all on GitHub

No open issues yet, or sync has not completed.

Highlights

  • •Getting started
  • •1. ⚛️ Declare
  • •2. Root
  • •3. ▶️ Call \& Await
  • •Advanced usage
  • •End from caller
  • •Exit animations
  • •Passing Root props
  • •Mutation flow
  • •Optional mutationFn

> Tags

TypeScriptdialogpatternreacttypescript

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category前端框架
PricingOpen source

> Related tools

R
React
用于构建用户界面的 JavaScript 库
V
Vue.js
渐进式 JavaScript 框架
N
Next.js
基于 React 的全栈 Web 框架