@askable-ui/react-native
React Native bindings for askable-ui.
This initial slice focuses on explicit mobile interactions: useAskable() provides a context backed by @askable-ui/core, useAskableScreen() lets you mirror screen focus into that context, useAskableScrollView() mirrors raw ScrollView measurement into that context, useAskableVisibility() mirrors viewability updates from mobile lists, and <Askable /> turns onPress / onLongPress interactions into prompt-ready focus updates.
A runnable Expo reference app lives in examples/react-native-expo.
Install
npm install @askable-ui/react-native @askable-ui/core<Askable>
Clones a single React Native pressable child and merges focus updates into its onPress and onLongPress handlers. Nested <Askable> wrappers also contribute ancestor segments, so press-driven mobile flows can serialize the same hierarchy paths as DOM-based web flows.
import { Pressable, Text } from 'react-native';
import { Askable, useAskable } from '@askable-ui/react-native';
function RevenueCard() {
const { ctx } = useAskable();
return (
<Askable ctx={ctx} meta={{ widget: 'revenue' }} text="Revenue card">
<Pressable>
<Text>Revenue</Text>
</Pressable>
</Askable>
);
}Props:
| Prop | Type | Description |
|---|---|---|
ctx | AskableContext | Context instance that receives focus updates |
meta | Record<string, unknown> | string | Metadata pushed into the context on press |
scope | string | Optional category stored with press-driven focus for scoped prompt/history queries |
text | string | Optional human-readable label stored alongside meta |
children | ReactElement | A single child that accepts onPress / onLongPress props |
<Askable ctx={ctx} meta={{ view: 'dashboard' }} text="Dashboard" scope="analytics">
<Askable ctx={ctx} meta={{ tab: 'finance' }} text="Finance" scope="analytics">
<Askable ctx={ctx} meta={{ metric: 'revenue' }} text="Revenue card" scope="analytics">
<Pressable>
<Text>Revenue</Text>
</Pressable>
</Askable>
</Askable>
</Askable>
ctx.toPromptContext();
// → "User is focused on: — view: dashboard > tab: finance > metric: revenue — value \"Revenue card\""useAskable(options?)
Returns a React Native-friendly wrapper around an AskableContext.
import { useAskable } from '@askable-ui/react-native';
const { focus, promptContext, ctx } = useAskable();Options:
| Option | Type | Description |
|---|---|---|
ctx | AskableContext | Reuse an existing context instead of creating a new one |
name | string | Optional context name passed through to createAskableContext() |
viewport | boolean | Optional viewport-aware core context mode |
events | AskableEvent[] | Forwarded to the underlying core context |
Returns:
| Value | Type | Description |
|---|---|---|
focus | AskableFocus | null | Current focus created by press-driven updates |
promptContext | string | Natural-language context string for your LLM prompt |
ctx | AskableContext | Full context instance for push(), clear(), toHistoryContext(), etc. |
Inspector
React Native does not use the floating browser overlay inspector. createAskableInspector() is DOM-based, so it is only relevant on web targets.
For React Native development, the equivalent debugging pattern is to inspect focus, promptContext, and ctx.toHistoryContext() directly in your own debug UI or logs:
function AskableDebugPanel() {
const { focus, promptContext } = useAskable();
return __DEV__ ? (
<View>
<Text>{focus ? JSON.stringify(focus.meta) : 'No focus yet'}</Text>
<Text>{promptContext}</Text>
</View>
) : null;
}If you share a custom ctx across a screen, read from that same ctx in your debug panel so the mobile debugging surface reflects the same Askable context as your press/visibility helpers.
Shared vs private/custom contexts
React Native differs from the web adapters: useAskable() creates a private context per hook call unless you provide ctx.
- Private default — each
useAskable()call gets its ownAskableContext. - Custom shared context — create one context and pass it to
useAskable({ ctx }),<Askable ctx={ctx} />,useAskableScreen({ ctx }),useAskableScrollView({ ctx }), anduseAskableVisibility({ ctx })when one screen or app surface should share focus/history. - Multiple isolated surfaces — create separate contexts when different tabs/screens/chats should not affect each other.
import { createAskableContext } from '@askable-ui/core';
import { Askable, useAskable, useAskableScreen } from '@askable-ui/react-native';
// Shared screen-level context
const screenCtx = createAskableContext();
function RevenueScreen() {
const askable = useAskable({ ctx: screenCtx });
useAskableScreen({
ctx: screenCtx,
meta: { screen: 'RevenueScreen' },
text: 'Revenue screen',
});
return (
<Askable ctx={screenCtx} meta={{ widget: 'revenue' }} text="Revenue card">
<Pressable>{/* ... */}</Pressable>
</Askable>
);
}
// Separate private context for another surface
function SupportChatScreen() {
const support = useAskable();
return null;
}Use the private default for isolated screens. Provide ctx explicitly when multiple React Native helpers/components should cooperate on one Askable context.
useAskableScreen(options)
Pushes screen-level context into the shared AskableContext while a screen is active.
import { useIsFocused } from '@react-navigation/native';
import { useAskable, useAskableScreen } from '@askable-ui/react-native';
function RevenueScreen() {
const isFocused = useIsFocused();
const { ctx } = useAskable();
useAskableScreen({
ctx,
active: isFocused,
meta: { screen: 'RevenueScreen' },
text: 'Revenue screen',
});
return null;
}Options:
| Option | Type | Description |
|---|---|---|
ctx | AskableContext | Reuse an existing context instead of creating a new one |
meta | Record<string, unknown> | string | Screen metadata pushed when the screen is active |
text | string | Optional label stored alongside the screen metadata |
active | boolean | Whether the screen is currently focused. Default: true |
clearOnBlur | boolean | Clear the context when the screen becomes inactive. Default: true |
name / viewport / events | Core options | Forwarded when the hook creates its own context |
useAskableScrollView(options)
Mirrors raw ScrollView measurement into the shared AskableContext by tracking child layouts and selecting the top visible measured item.
import { Pressable, ScrollView, Text } from 'react-native';
import { Askable, useAskable, useAskableScrollView } from '@askable-ui/react-native';
const cards = [
{ id: 'rev', title: 'Revenue', meta: { widget: 'revenue' } },
{ id: 'pipe', title: 'Pipeline', meta: { widget: 'pipeline' } },
];
function DashboardFeed() {
const { ctx } = useAskable();
const { onScroll, createOnItemLayout } = useAskableScrollView({
ctx,
getMeta: (card) => ({ ...card.meta, visible: true }),
getText: (card) => `${card.title} is leading the dashboard scroll view`,
});
return (
<ScrollView onScroll={onScroll} scrollEventThrottle={16}>
{cards.map((card) => (
<Askable key={card.id} ctx={ctx} meta={card.meta} text={card.title}>
<Pressable onLayout={createOnItemLayout(card.id, card)}>
<Text>{card.title}</Text>
</Pressable>
</Askable>
))}
</ScrollView>
);
}Options:
| Option | Type | Description |
|---|---|---|
ctx | AskableContext | Reuse an existing context instead of creating a new one |
active | boolean | Whether scroll updates should currently affect focus. Default: true |
clearOnBlur | boolean | Clear existing scroll focus when tracking becomes inactive. Default: true |
getMeta | (item, measured) => Record<string, unknown> | string | Maps the visible measured item into askable metadata |
getText | (item, measured) => string | Optional human-readable label stored alongside the metadata |
selectVisible | (items) => item | null | Override which measured item should win focus. Default: the top-most visible item |
name / viewport / events | Core options | Forwarded when the hook creates its own context |
Returns:
| Value | Type | Description |
|---|---|---|
ctx | AskableContext | Context instance used for scroll-driven focus updates |
onScroll | (event) => void | Attach to ScrollView.onScroll |
measureItem | (key, item, layout) => void | Manually register/update a child layout |
unmeasureItem | (key) => void | Remove a child layout when it leaves the tree |
clearVisibleItem | () => void | Clear the current scroll-driven focus |
createOnItemLayout | (key, item) => onLayoutHandler | Convenience helper for onLayout wiring |
useAskableVisibility(options)
Mirrors FlatList / SectionList viewability callbacks into the shared AskableContext.
import { FlatList, Text, View } from 'react-native';
import { useAskable, useAskableVisibility } from '@askable-ui/react-native';
const rows = [
{ id: 'deal-1', title: 'Enterprise renewal' },
{ id: 'deal-2', title: 'Expansion pipeline' },
];
function DealsList() {
const { ctx } = useAskable();
const { onViewableItemsChanged } = useAskableVisibility({
ctx,
getMeta: (item) => ({ dealId: item.id }),
getText: (item) => item.title,
});
return (
<FlatList
data={rows}
keyExtractor={(item) => item.id}
onViewableItemsChanged={onViewableItemsChanged}
renderItem={({ item }) => (
<View>
<Text>{item.title}</Text>
</View>
)}
/>
);
}Options:
| Option | Type | Description |
|---|---|---|
ctx | AskableContext | Reuse an existing context instead of creating a new one |
active | boolean | Whether visibility updates should currently affect focus. Default: true |
clearOnBlur | boolean | Clear existing visibility focus when tracking becomes inactive. Default: true |
getMeta | (item, token) => Record<string, unknown> | string | Maps the visible row into askable metadata |
getText | (item, token) => string | Optional human-readable label stored alongside the metadata |
selectViewable | (tokens) => token | null | Override which visible token should win focus. Default: the first visible item |
name / viewport / events | Core options | Forwarded when the hook creates its own context |
Notes
- This adapter currently covers press-driven interactions, lightweight screen-awareness, raw
ScrollViewmeasurement, and list viewability callbacks. useAskableScreen()is designed to pair with React Navigation'suseIsFocused()or a similar focus signal.useAskableScrollView()is a good fit for custom dashboard layouts built withScrollView.useAskableVisibility()is ideal forFlatList/SectionList.- Existing child
onPress/onLongPresshandlers are preserved and still run.