An implementation guide for React Native applications, covering model catalogs, accessible selection, state management, and backend validation.
An AI model selector can look like a simple list of names. In practice, it controls a decision that affects response quality, supported inputs, latency, and cost.
A text-only model should not appear usable when someone attaches an image. A saved preference should not remain valid after access changes. Selecting another model should not relabel a response that is already being generated.
Building this interface requires more than styling a dropdown.
This guide uses React Native components and TypeScript for mobile applications. The underlying React state patterns also apply to web applications, although browser interfaces should use appropriate HTML controls.
Start With an Application-Owned Model Catalog
The interface should receive model options from the application’s backend. That catalog should describe what the current user can actually use, rather than simply reproducing a provider’s full model list.
A useful catalog separates three concerns:
- Identity: A stable application identifier and readable name.
- Compatibility: Input types supported by the application’s integration.
- Availability: Whether the option is currently selectable, with a reason when it is not.
For example:
type InputKind = "text" | "image";
type ModelOption = {
id: string;
name: string;
description: string;
inputKinds: InputKind[];
available: boolean;
unavailableReason?: string;
};The following entries are fictional application options, not claims about real provider models:
const models: ModelOption[] = [
{
id: "text-standard",
name: "Everyday text",
description: "For drafting and summarizing text.",
inputKinds: ["text"],
available: true,
},
{
id: "image-assistant",
name: "Image assistant",
description: "For questions about text and images.",
inputKinds: ["text", "image"],
available: true,
},
];In production, the backend maps these identifiers to approved provider configurations. Capabilities should reflect the implemented endpoint, account permissions, and product restrictions.
A provider supporting image input does not automatically mean the application’s integration supports it.
Keep Provider Credentials on the Backend
A React Native application should call an authenticated application backend, which then communicates with the model provider.
React Native’s security documentation warns that secrets bundled into an application can be recovered and recommends a server-side orchestration layer for protecting sensitive credentials. OpenAI’s API documentation similarly instructs developers not to expose API keys in browsers or apps.
Two application endpoints might be:
GET /ai/models
POST /ai/generationsThe first returns the user’s permitted catalog. The second accepts a model identifier and generation input.
For every generation, the backend should independently check authorization, model availability, input compatibility, and applicable usage limits. Disabling a card in the interface improves usability; it does not enforce access control.
Store the Selected ID, Then Derive the Model
React recommends avoiding redundant state and calculating values from existing props or state when possible. Its state-structure guidance specifically illustrates storing a selected item’s ID instead of duplicating the selected object.
Apply that pattern here:
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedModel =
models.find((model) => model.id === selectedId) ?? null;If the catalog changes, the derived object reflects the current metadata. A separately stored model object could retain an outdated availability flag or description.
Eligibility should also be derived:
function getDisabledReason(
model: ModelOption,
requiredInputs: InputKind[],
): string | null {
if (!model.available) {
return model.unavailableReason ?? "Currently unavailable.";
}
const incompatible = requiredInputs.some(
(input) => !model.inputKinds.includes(input),
);
return incompatible
? "Does not support the current input."
: null;
}This function expresses application policy. It is not a replacement for server validation.
Build an Accessible React Native Selector
For a short catalog, selectable cards can expose more useful information than a compact dropdown. Each card can show a name, description, selected state, and reason for being unavailable.
React Native documents radio accessibility roles and checked and disabled accessibility states. These allow assistive technologies to identify the control and its state.
The following component assumes the type and helper function above are in scope:
import { FlatList, Pressable, Text, View } from "react-native";
type ModelSelectorProps = {
models: ModelOption[];
selectedId: string | null;
requiredInputs: InputKind[];
onSelect: (id: string) => void;
};
export function ModelSelector({
models,
selectedId,
requiredInputs,
onSelect,
}: ModelSelectorProps) {
return (
<View style={{ flex: 1 }}>
<Text
accessibilityRole="header"
style={{ fontSize: 22, fontWeight: "600", marginBottom: 12 }}
>
Choose an AI model
</Text>
<FlatList
data={models}
keyExtractor={(item) => item.id}
extraData={{ selectedId, requiredInputs }}
ListEmptyComponent={
<Text>No models are available for this account.</Text>
}
renderItem={({ item }) => {
const reason = getDisabledReason(item, requiredInputs);
const disabled = reason !== null;
const checked = item.id === selectedId;
return (
<Pressable
accessible
accessibilityRole="radio"
accessibilityLabel={[
item.name,
item.description,
reason,
]
.filter(Boolean)
.join(". ")}
accessibilityState={{ checked, disabled }}
disabled={disabled}
onPress={() => onSelect(item.id)}
style={({ pressed }) => ({
padding: 16,
marginBottom: 12,
borderWidth: checked ? 2 : 1,
borderColor: checked ? "#174EA6" : "#687386",
borderRadius: 12,
backgroundColor: pressed ? "#EAF1FF" : "#FFFFFF",
minHeight: 64,
})}
>
<Text style={{ fontSize: 17, fontWeight: "600" }}>
{item.name}
{checked ? " — Selected" : ""}
</Text>
<Text style={{ marginTop: 6 }}>
{item.description}
</Text>
{reason ? (
<Text style={{ marginTop: 6, color: "#8B1E1E" }}>
{reason}
</Text>
) : null}
</Pressable>
);
}}
/>
</View>
);
}The component’s parent should give it bounded vertical space. A few options can also be rendered with a simple mapped list; virtualization is more useful as the catalog grows.
FlatList uses shallow prop comparisons. Its documentation recommends extraData when row rendering depends on state outside data. Here, selection and input requirements both affect the rows.
The implementation also uses visible text to identify selection instead of relying on border color alone. Screen-reader behavior should still be tested with VoiceOver and TalkBack.
Handle Catalog Changes Without Silent Substitution
Suppose a user selects a text model, then attaches an image.
The selected ID can remain visible as their existing choice, but submission should become unavailable with an explanation. Automatically switching models may unexpectedly change cost or behavior.
A derived validation message can cover several cases:
const selectionProblem =
selectedId === null
? "Choose a model."
: selectedModel === null
? "The selected model is no longer available."
: getDisabledReason(selectedModel, requiredInputs);
const canSubmit =
catalogStatus === "ready" &&
selectionProblem === null &&
prompt.trim().length > 0 &&
!isSubmitting;Here, catalogStatus, prompt, and isSubmitting belong to the surrounding screen.
A persisted model ID should be treated as a preference requiring revalidation. The same applies when the user changes accounts, workspaces, or subscription plans.
Catalog loading should distinguish an empty result from a network failure. “No models available” and “Could not load models” require different recovery actions.
For asynchronous catalog requests, React’s documentation recommends aborting a fetch or ignoring its result during Effect cleanup to prevent stale responses from updating the interface. This is particularly relevant when users switch workspaces while requests are pending.
Separate the Picker From the Active Generation
Once generation starts, the request should capture the selected model ID.
For example, an application-level request might contain:
{
"modelId": "text-standard",
"prompt": "Summarize this meeting."
}The backend can return application metadata such as:
{
"requestId": "generation-123",
"requestedModelId": "text-standard",
"resolvedModelId": "provider-model-version",
"output": "..."
}These are illustrative API fields, not a provider-specific response format.
The response label should use request metadata rather than the current picker state. Otherwise, selecting another option during generation can make an existing response appear to come from the wrong model.
For an initial implementation, locking the selector during generation simplifies this behavior. A more flexible interface can allow selection changes while clearly stating that they apply to the next request.
If server-side fallback is supported, its behavior should be explicit. Routing to another provider can affect privacy expectations, capabilities, and cost.
Explain Cost and Performance Carefully
Labels such as “fastest” or “best” need evidence from the application’s workload.
A more defensible interface shows a short task description and, where available, measured latency or a clearly labeled cost estimate.
For a text request, a simplified token-cost estimate is:
[
\text{Estimated cost} =
\frac{\text{input tokens} \times \text{input rate}}{1{,}000{,}000}
+
\frac{\text{estimated output tokens} \times \text{output rate}}{1{,}000{,}000}
]
This assumes rates quoted per million tokens. Actual billing may involve cached input, tools, media, or other provider-specific charges.
Output length is unknown before completion, so the interface should not present an estimate as a guaranteed charge. Pricing metadata should be maintained server-side and refreshed against the relevant provider’s documentation.
Test the Decisions Behind the Interface
The most valuable checks exercise changing conditions:
- A selected model disappears after a catalog refresh.
- An attachment makes the current selection incompatible.
- An older workspace request finishes after a newer one.
- The backend rejects a model that looked available moments earlier.
- Selection changes while an earlier response is still streaming.
- A saved preference belongs to a different account.
- VoiceOver or TalkBack announces selection and unavailability correctly.
- Larger text remains readable without clipping.
The example component is an instructional starting point, not a complete production application. Authentication, runtime API-response validation, catalog caching, generation transport, and error handling belong in the surrounding implementation.
A dependable AI model selection interface keeps three things consistent: what the user selected, what the backend permits, and what actually served the request.





















Add Comment