react-call v2

Item picker

Show a list and resolve with the chosen item. Caller-side cancellation returns null; selecting an item returns the object itself.

→ nothing picked yet

The Callable

declared once, mounted in the React tree
Picker.tsx
import { createCallable } from 'react-call'
interface Item {
id: string
name: string
hint?: string
}
interface Props {
title: string
items: readonly Item[]
}
export const Picker = createCallable<Props, Item | null>(
({ call, title, items }) => (
<div
role="dialog"
aria-modal="true"
aria-label={title}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
>
<div className="w-full max-w-sm rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] p-2 shadow-2xl">
<div className="flex items-center justify-between px-3 py-2">
<p className="text-sm font-medium text-[var(--color-fg)]">{title}</p>
<button
type="button"
onClick={() => call.end(null)}
aria-label="Cancel"
className="-mr-1 inline-flex h-7 w-7 items-center justify-center rounded-md text-base leading-none text-[var(--color-fg-subtle)] transition-colors hover:bg-[var(--color-bg-subtle)] hover:text-[var(--color-fg)]"
>
×
</button>
</div>
<ul className="max-h-72 overflow-y-auto">
{items.map((item) => (
<li key={item.id}>
<button
type="button"
onClick={() => call.end(item)}
className="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-left text-sm text-[var(--color-fg)] transition-colors hover:bg-[var(--color-bg-subtle)]"
>
<span>{item.name}</span>
{item.hint && (
<span className="font-mono text-xs text-[var(--color-fg-subtle)]">
{item.hint}
</span>
)}
</button>
</li>
))}
</ul>
</div>
</div>
),
)
Picker.displayName = 'Picker'

The Root

mounted in your app tree, once
App.tsx
import { Picker } from './Picker'
import { FruitPickerTrigger } from './FruitPickerTrigger'
export default function App() {
return (
<>
<Picker />
<FruitPickerTrigger />
</>
)
}

The caller

anywhere in your app, imperative
FruitPickerTrigger.tsx
import { useState } from 'react'
import { Picker } from './Picker'
const FRUITS = [
{ id: 'apple', name: 'Apple', hint: '🍎' },
{ id: 'banana', name: 'Banana', hint: '🍌' },
{ id: 'cherry', name: 'Cherry', hint: '🍒' },
{ id: 'grape', name: 'Grape', hint: '🍇' },
{ id: 'mango', name: 'Mango', hint: '🥭' },
] as const
export const FruitPickerTrigger = () => {
const [picked, setPicked] = useState<string | null>(null)
const handleClick = async () => {
const choice = await Picker.call({
title: 'Pick a fruit',
items: FRUITS,
})
setPicked(choice ? choice.name : null)
}
return (
<div className="flex flex-col items-center gap-3">
<button
type="button"
onClick={handleClick}
className="rounded-md bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-[var(--color-accent-fg)] transition-colors hover:bg-[var(--color-accent-hover)]"
>
Pick a fruit
</button>
<span className="font-mono text-xs text-[var(--color-fg-subtle)]">
{picked ? (
<span className="text-[var(--color-accent)]">→ {picked}</span>
) : (
'→ nothing picked yet'
)}
</span>
</div>
)
}

Related examples