`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