@pushframe/sdk
v0.1.1
Published
Pushframe React Native SDK — Server-Driven UI rendering engine
Downloads
161
Maintainers
Readme
@pushframe/sdk
React Native SDK for PushFrame — server-driven UI rendering engine that fetches and renders UI schemas at runtime without app updates.
Installation
npm install @pushframe/sdk
# or
yarn add @pushframe/sdkPeer dependencies (must be installed in your project):
npm install react react-nativeOptionally, if you use react-native-safe-area-context, the safeareaview component will automatically prefer it over the React Native core fallback.
Quick Start
Wrap your app with PushFrame.Provider, then drop PushFrame.Screen or PushFrame.Component wherever you want server-driven UI to appear.
import { PushFrame } from '@pushframe/sdk';
export default function App() {
return (
<PushFrame.Provider apiKey="your-api-key" appVersion="1.0.0">
<PushFrame.Screen id="home" />
</PushFrame.Provider>
);
}Provider
PushFrame.Provider is the root wrapper. Place it at the top of your component tree.
<PushFrame.Provider
apiKey="your-api-key"
appVersion="1.0.0"
baseUrl="https://api.pushframe.io"
context={{ user: { name: 'Alice', isAdmin: true } }}
components={{ MyCustomCard }}
loadingComponent={<MySpinner />}
fallbackComponent={<MyErrorView />}
onAction={(action, payload) => {
if (action === 'navigate') {
navigation.navigate(payload.screen);
}
}}
onError={(error) => console.error(error)}
>
{/* your app */}
</PushFrame.Provider>Provider Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| apiKey | string | Yes | — | API key used for authentication (Authorization and x-project-key headers). |
| appVersion | string | No | undefined | Semver version string sent in fetch URL path. When omitted, the literal "null" is sent. Pass this explicitly — auto-detection is not supported. |
| baseUrl | string | No | https://api.pushframe.io | Override the API base URL. |
| context | Record<string, unknown> | No | {} | Global runtime data available to all binding expressions. |
| components | Record<string, React.ComponentType> | No | {} | Register custom components by type name, available to the renderer. |
| loadingComponent | ReactNode | No | null | Shown while a schema is being fetched. |
| fallbackComponent | ReactNode | No | null | Shown when a schema fetch fails. |
| onAction | (action: string, payload?) => void | No | — | Called for any action that is not handled internally (see Actions). |
| onError | (error: Error) => void | No | — | Called when a schema fetch or render error occurs. |
| children | ReactNode | Yes | — | Your application content. |
Slot Components
PushFrame.Screen
Fetches and renders a full-page screen schema. Uses flex: 1 layout.
<PushFrame.Screen
id="home"
context={{ currentTab: 'feed' }}
onAction={(action, payload) => {
if (action === 'navigate') {
navigation.navigate(payload.screen);
return true; // stop bubbling to Provider
}
}}
/>Fetches from: GET {baseUrl}/screens/{id}/{appVersion}
PushFrame.Component
Fetches and renders an inline component schema. Renders inline without forced flex.
<PushFrame.Component
id="product-card"
context={{ productId: '123' }}
/>Fetches from: GET {baseUrl}/components/{id}/{appVersion}
Slot Props
Both components share these props:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| id | string | Yes | Screen or component ID. |
| context | Record<string, unknown> | No | Local context merged with (and overriding) the Provider context. |
| isLoading | boolean | No | Externally controlled loading state. |
| loadingComponent | ReactNode | No | Overrides the Provider-level loading UI. |
| fallbackComponent | ReactNode | No | Overrides the Provider-level fallback UI. |
| onAction | (action, payload?) => boolean \| void | No | Local action handler. Return true to stop the action from bubbling to the Provider. |
Schema Format
The API server must return schemas in this envelope format:
{
"schema": {
"type": "view",
"props": { "style": { "flex": 1, "padding": 16 } },
"children": [
{
"type": "text",
"props": { "value": "Hello {{user.name}}" }
}
]
}
}SchemaNode
interface SchemaNode {
id?: string;
type: string; // Component type (see built-in types below)
props?: Record<string, unknown>; // Props passed to the component
children?: SchemaNode[]; // Nested nodes
actions?: Action[]; // Event → action mappings
if?: string; // Binding expression; falsy = node is hidden
}
interface Action {
trigger: string; // e.g. "onPress", "onChange", "onLongPress"
action: string; // Action name (built-in or custom)
payload?: Record<string, unknown>;
}Built-in Component Types
| Type | React Native Equivalent | Notes |
|------|------------------------|-------|
| view | View | Layout container |
| scrollview | ScrollView | Scrollable container; supports scroll-to action |
| text | Text | Use value prop for text content |
| image | Image | Use src prop for URI strings |
| pressable | Pressable | Triggers onPress, onLongPress |
| textinput | TextInput | onChange → onChangeText, onSubmit → onSubmitEditing |
| flatlist | FlatList | Requires items array and renderItem node (see below) |
| modal | Modal | |
| activityindicator | ActivityIndicator | Spinner |
| switch | Switch | onChange → onValueChange |
| keyboardavoidingview | KeyboardAvoidingView | |
| safeareaview | SafeAreaView | Prefers react-native-safe-area-context when available |
| statusbar | StatusBar | |
Data Binding
Props support {{expression}} syntax resolved against the runtime context.
Full binding — resolves to the actual value (any type):
{ "type": "text", "props": { "value": "{{user.name}}" } }Inline binding — string interpolation:
{ "type": "text", "props": { "value": "Hello, {{user.name}}!" } }Nested paths:
{ "type": "image", "props": { "src": "{{product.imageUrl}}" } }Unresolved paths silently return undefined — they never cause a crash.
Conditional Rendering
Use the if field with a binding expression to show or hide a node:
{
"type": "text",
"props": { "value": "Admin Panel" },
"if": "{{user.isAdmin}}"
}Falsy values (false, null, undefined, 0, "") hide the node and its entire subtree.
Actions
Actions link UI events to behaviour.
{
"type": "pressable",
"actions": [
{
"trigger": "onPress",
"action": "show-toast",
"payload": { "message": "Saved!", "type": "success" }
}
],
"children": [
{ "type": "text", "props": { "value": "Save" } }
]
}Built-in Actions
These are handled internally and do not reach your onAction callback.
| Action | Payload |
|--------|---------|
| show-toast | message: string, duration?: number (ms), type?: "success" \| "error" \| "info" \| "warning" |
| show-bottom-sheet | schema: SchemaNode, context?: Record<string, unknown> |
| dismiss-bottom-sheet | (none) |
| scroll-to | x?: number, y?: number, animated?: boolean |
All other action names bubble to the slot's onAction, then to the Provider's onAction if the slot handler does not return true.
FlatList
flatlist nodes require an items array in props and a renderItem node as a sibling field (not inside children). The renderer injects { item, index } into the context for each rendered item.
{
"type": "flatlist",
"props": {
"items": "{{products}}",
"direction": "vertical"
},
"renderItem": {
"type": "view",
"props": { "style": { "padding": 8 } },
"children": [
{ "type": "text", "props": { "value": "{{item.name}}" } },
{ "type": "text", "props": { "value": "{{item.price}}" } }
]
}
}| Prop | Type | Description |
|------|------|-------------|
| items | array \| string | Array of data items (or binding expression resolving to one). |
| direction | "vertical" \| "horizontal" | Maps to the horizontal prop. Default: "vertical". |
| numColumns | number | Passed through to FlatList. |
| keyExtractor | string | Binding expression evaluated per item (e.g. "{{item.id}}"). |
Custom Components
Register your own React Native components and reference them by type name in schemas.
import { PushFrame } from '@pushframe/sdk';
function ProductCard({ title, price }: { title: string; price: string }) {
return (
<View>
<Text>{title}</Text>
<Text>{price}</Text>
</View>
);
}
<PushFrame.Provider
apiKey="..."
components={{ 'product-card': ProductCard }}
>
{/* Schema can now use "type": "product-card" */}
</PushFrame.Provider>Built-in type names are reserved and cannot be overridden.
Context Merging
Context flows from Provider → Slot, with the Slot's context taking precedence on key conflicts.
// Provider sets global context
<PushFrame.Provider context={{ user: { name: 'Alice' }, theme: 'dark' }}>
{/* Slot adds/overrides local context */}
<PushFrame.Screen
id="profile"
context={{ pageTitle: 'My Profile', theme: 'light' }} // overrides theme
/>
</PushFrame.Provider>TypeScript
The SDK is written in TypeScript. Key types are exported for use in your own code:
import type {
SchemaNode,
Action,
PushFrameProviderProps,
PushFrameSlotProps,
ToastPayload,
BottomSheetPayload,
} from '@pushframe/sdk';Advanced: Accessing Context
Use the usePushFrameContext hook inside any component rendered within PushFrame.Provider to access SDK internals.
import { usePushFrameContext } from '@pushframe/sdk';
function MyButton() {
const { showToast } = usePushFrameContext();
return (
<Button onPress={() => showToast({ message: 'Hello!', type: 'info' })} />
);
}API Reference
Request Format
Every schema fetch includes these headers:
Authorization: Bearer {apiKey}
x-project-key: {apiKey}
Accept: application/jsonIDs and appVersion are encodeURIComponent-encoded in the URL path. When appVersion is not set on the Provider, the literal string "null" is sent (e.g. GET /screens/home/null).
Response Format
{
"schema": { },
"version": 28,
"status": "published",
"publishedAt": "2024-01-01T00:00:00Z",
"targetMinVersion": "0.0.0"
}The SDK extracts response.schema automatically. A bare SchemaNode (without the envelope) is also accepted as a fallback.
License
MIT
