Skip to content

Custom Components

Register any React Native component. The LLM learns it automatically from the Zod schema. No manual prompt engineering needed.

1

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.

components/MoodPicker.tsx
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});
2

Build the React Native component

Declare the callbacks you need from InjectedProps. Use onSubmit for value submission, onSelect for selections, onContinue for CTAs.

components/MoodPicker.tsx
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});
3

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.

components/MoodPicker.tsx
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};
Write good descriptions
The description is injected directly into the system prompt. A good description tells the LLM: (1) when to use this component, (2) what makes it different from similar built-in components, (3) any usage rules.
4

Register with WireAIProvider

App.tsx
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:

Ts
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 typeIn system promptExample
z.string().describe("...")Shows description texttitle: Card heading
z.string().optional()Listed without required markerbody (optional): ...
z.number()LLM knows to use a numbercount: A count
z.boolean()LLM knows to use true/falseenabled: Toggle
z.array(z.string())LLM knows to use an arraychips: Array of labels
z.enum(["a","b"])LLM sees valid valuesstatus: 'success'|'error'|'info'
Always add .describe()
Without .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:

LLM JSON for MoodPicker
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}