Custom Components
Register any React Native component. The LLM learns it automatically from the Zod schema. No manual prompt engineering needed.
Define the props schema with Zod
Every field should have a .describe(). These descriptions are injected directly into the system prompt so the LLM knows how to use each prop.
1import { z } from "zod";
2
3const schema = z.object({
4 question: z.string().describe("The question to ask the user"),
5 instruction: z.string().optional().describe("Instruction below the question"),
6});Build the React Native component
Declare the callbacks you need from InjectedProps. Use onSubmit for value submission, onSelect for selections, onContinue for CTAs.
1import React, { useState, useCallback } from "react";
2import { Pressable, Text, View, StyleSheet } from "react-native";
3import type { InjectedProps } from "wireai-rn";
4import { colors, spacing, radii, textStyles } from "wireai-rn";
5
6type Props = {
7 question: string;
8 instruction?: string;
9} & InjectedProps;
10
11const MOODS = [
12 { emoji: "๐", label: "Great" },
13 { emoji: "๐", label: "Good" },
14 { emoji: "๐", label: "Okay" },
15 { emoji: "๐", label: "Low" },
16 { emoji: "๐ข", label: "Struggling" },
17];
18
19const _MoodPicker: React.FC<Props> = ({ question, instruction, onSubmit }) => {
20 const [selected, setSelected] = useState<string | null>(null);
21
22 const handleSubmit = useCallback(() => {
23 if (selected) onSubmit?.(selected);
24 }, [selected, onSubmit]);
25
26 return (
27 <View style={styles.card}>
28 <Text style={styles.question}>{question}</Text>
29 {instruction && <Text style={styles.instruction}>{instruction}</Text>}
30 <View style={styles.row}>
31 {MOODS.map(({ emoji, label }) => (
32 <Pressable
33 key={label}
34 style={[styles.chip, selected === label && styles.chipSelected]}
35 onPress={() => setSelected(label)}
36 >
37 <Text style={styles.emoji}>{emoji}</Text>
38 <Text style={styles.label}>{label}</Text>
39 </Pressable>
40 ))}
41 </View>
42 {selected && (
43 <Pressable style={styles.btn} onPress={handleSubmit}>
44 <Text style={styles.btnText}>Submit</Text>
45 </Pressable>
46 )}
47 </View>
48 );
49};
50
51const styles = StyleSheet.create({
52 card: {
53 padding: spacing.md,
54 backgroundColor: colors.backgroundSecondary,
55 borderRadius: radii.md,
56 borderWidth: 1,
57 borderColor: colors.border,
58 gap: spacing.sm,
59 },
60 question: { ...textStyles.h4, color: colors.text },
61 instruction: { ...textStyles.bodySmall, color: colors.textSecondary },
62 row: { flexDirection: "row", flexWrap: "wrap", gap: spacing.sm },
63 chip: {
64 alignItems: "center",
65 padding: spacing.sm,
66 borderRadius: radii.md,
67 borderWidth: 1,
68 borderColor: colors.border,
69 minWidth: 60,
70 },
71 chipSelected: {
72 borderColor: colors.primary,
73 backgroundColor: colors.primaryBackground,
74 },
75 emoji: { fontSize: 28 },
76 label: { ...textStyles.caption, color: colors.textSecondary, marginTop: 4 },
77 btn: {
78 backgroundColor: colors.primary,
79 borderRadius: radii.md,
80 paddingVertical: 12,
81 alignItems: "center",
82 },
83 btnText: { color: "#fff", fontWeight: "600" as const, fontSize: 14 },
84});Export as a WireAIComponent
The description field is critical. It tells the LLM when to use this component. Be specific: describe the use case, not just what the component looks like.
1import type { WireAIComponent } from "wireai-rn";
2
3export const MoodPicker: WireAIComponent = {
4 name: "MoodPicker",
5 description: "Use at the start of a check-in to ask how the user is feeling. Shows 5 mood options with emoji. Always use this for mood questions, never SelectionCard.",
6 component: React.memo(_MoodPicker),
7 propsSchema: schema,
8};Register with WireAIProvider
1import { defaultComponents } from "wireai-rn";
2import { MoodPicker } from "./components/MoodPicker";
3
4const components = [...defaultComponents, MoodPicker];
5
6<WireAIProvider llm={config} components={components}>
7 {children}
8</WireAIProvider>The SDK automatically regenerates the system prompt to include your new component. The LLM will start using MoodPicker in appropriate situations.
InjectedProps reference
Callbacks from useWireAIAction are automatically merged into your component's props by ComponentRenderer. Declare only the ones you use:
1import type { InjectedProps } from "wireai-rn";
2
3// Full interface:
4interface InjectedProps {
5 onSubmit?: (value: unknown) => void; // form value submission
6 onSelect?: (value: unknown) => void; // single/multi selection
7 onConfirm?: (payload?: unknown) => void; // yes/confirm
8 onDeny?: () => void; // no/cancel
9 onPress?: (label: string) => void; // labeled button press
10 onContinue?: () => void; // CTA / continue
11 onCancel?: () => void; // cancel / dismiss
12}Zod type โ system prompt mapping
wireai-rn reads your Zod schema at runtime to generate the system prompt documentation. Here's how Zod types translate:
| Zod type | In system prompt | Example |
|---|---|---|
| z.string().describe("...") | Shows description text | title: Card heading |
| z.string().optional() | Listed without required marker | body (optional): ... |
| z.number() | LLM knows to use a number | count: A count |
| z.boolean() | LLM knows to use true/false | enabled: Toggle |
| z.array(z.string()) | LLM knows to use an array | chips: Array of labels |
| z.enum(["a","b"]) | LLM sees valid values | status: 'success'|'error'|'info' |
.describe(), the system prompt will show propName: any, and the LLM may hallucinate values. Describe every prop, especially optional ones.Full example: MoodPicker
The mental-coach example uses a custom MoodTracker component alongside all 11 built-in components. With a good description, the LLM uses it at exactly the right step in the flow:
1{
2 "action": "render",
3 "component": "MoodPicker",
4 "props": {
5 "question": "How are you feeling right now?",
6 "instruction": "Tap the emoji that best matches your mood"
7 },
8 "message": "Let's start with how you're feeling today."
9}