docs-site/content/guide/react-native-search-bar.md
This guide walks you through building a native mobile search interface using React Native (Expo) and Typesense.
You'll create a cross-platform book search application that works on both iOS and Android devices, demonstrating how to integrate Typesense into your React Native projects.
Mobile users expect instant results and won't wait around for slow searches. That's why combining React Native's native performance with Typesense's lightning-fast search engine creates the perfect foundation for a mobile app search experience that your users will love.
Typesense is a lightning-fast, typo-tolerant search engine that makes it easy to add powerful search to your applications. Think of it as your personal search assistant that understands what users are looking for, even when they make mistakes.
Here's a real-world scenario: imagine you're building a food delivery app like DoorDash or Uber Eats. A hungry user opens your app at midnight and searches for "piza hut" (with typos). Instead of showing "no restaurants found" and sending them to a competitor's app, Typesense instantly understands they meant "Pizza Hut" and shows nearby locations with delivery times. That split-second difference between a successful search and a frustrated user can make or break your app's retention. That's the magic of intelligent search!
Why developers choose Typesense:
This guide will use React Native with Expo, a framework that makes it easy to build native mobile apps using JavaScript and React.
Please ensure you have the following installed on your machine before proceeding:
This guide will use a Linux environment, but you can adapt the commands to your operating system.
Once Docker is installed, you can run a Typesense container in the background using the following commands:
Create a folder that will store all searchable data stored for Typesense:
mkdir "$(pwd)"/typesense-data
Run the Docker container:
<Tabs :tabs="['Shell']"> <template v-slot:Shell> <div class="manual-highlight"> <pre class="language-bash"><code>export TYPESENSE_API_KEY=xyz docker run -p 8108:8108 \ -v"$(pwd)"/typesense-data:/data typesense/typesense:{{ $site.themeConfig.typesenseLatestVersion }} \ --data-dir /data \ --api-key=$TYPESENSE_API_KEY \ --enable-cors \ -d</code></pre> </div> </template> </Tabs>Verify if your Docker container was created properly:
docker ps
You should see the Typesense container running without any issues:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82dd6bdfaf66 typesense/typesense:latest "/opt/typesense-serv…" 1 min ago Up 1 minutes 0.0.0.0:8108->8108/tcp, [::]:8108->8108/tcp nostalgic_babbage
That's it! You are now ready to create collections and load data into your Typesense server.
:::tip You can also set up a managed Typesense cluster on Typesense Cloud for a fully managed experience with a management UI, high availability, globally distributed search nodes and more. :::
Typesense needs you to create a <RouterLink :to="`/${$site.themeConfig.typesenseLatestVersion}/api/collections.html`">collection</RouterLink> in order to search through documents. A collection is a named container that defines a schema and stores indexed documents for search. Collection bundles three things together:
You can create the books collection for this project using this curl command:
curl "http://localhost:8108/collections" \
-X POST \
-H "Content-Type: application/json" \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
-d '{
"name": "books",
"fields": [
{"name": "title", "type": "string", "facet": false},
{"name": "authors", "type": "string[]", "facet": true},
{"name": "publication_year", "type": "int32", "facet": true},
{"name": "average_rating", "type": "float", "facet": true},
{"name": "image_url", "type": "string", "facet": false},
{"name": "ratings_count", "type": "int32", "facet": true}
],
"default_sorting_field": "ratings_count"
}'
Now that the collection is set up, we can load the sample dataset.
Download the sample dataset:
curl -O https://dl.typesense.org/datasets/books.jsonl.gz
Unzip the dataset:
gunzip books.jsonl.gz
Load the dataset in to Typesense:
curl "http://localhost:8108/collections/books/documents/import" \
-X POST \
-H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
--data-binary @books.jsonl
You should see a bunch of success messages if the data load is successful.
Now you're ready to actually build the application.
Create a new Expo project using this command:
npx create-expo-app@latest typesense-react-native-search-bar --template blank-typescript
This will scaffold a new React Native project with TypeScript support using Expo.
Once your project scaffolding is ready, navigate to the project directory and install the required dependencies:
cd typesense-react-native-search-bar
npm install
npm i react-instantsearch-core typesense-instantsearch-adapter
Let's go over the key dependencies:
:::tip Note
React Native can use react-instantsearch-core with the typesense-instantsearch-adapter, just like web frameworks. This gives you access to powerful InstantSearch hooks and widgets. Alternatively, you can make direct API calls to Typesense using the Fetch API for a lighter implementation.
:::
Let's create the project structure step by step. After each step, we'll show you how the directory structure evolves.
After creating the basic Expo app and installing the required dependencies, your project structure should look like this:
typesense-react-native-search-bar/
├── node_modules/
├── assets/
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ └── splash-icon.png
├── App.tsx
├── app.json
├── package-lock.json
├── package.json
└── tsconfig.json
Create the project directories:
mkdir -p components types utils
Your project structure should now look like this:
typesense-react-native-search-bar/
├── assets/
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ └── splash-icon.png
├── components/
├── types/
├── utils/
├── App.tsx
├── app.json
├── package-lock.json
├── package.json
└── tsconfig.json
Create the environment configuration file .env:
touch .env
Add this content to .env:
EXPO_PUBLIC_TYPESENSE_API_KEY=xyz
EXPO_PUBLIC_TYPESENSE_HOST=10.0.2.2
EXPO_PUBLIC_TYPESENSE_PORT=8108
EXPO_PUBLIC_TYPESENSE_PROTOCOL=http
:::tip Important
10.0.2.2 as the host (this maps to localhost on your machine)localhost as the host192.168.1.100) or use a tunneling service like ngrokExpo automatically loads environment variables starting with EXPO_PUBLIC_ and makes them available to your app via process.env.
:::
Using env variables is NOT recommended for production apps since they are bundled with the app. Instead, you want to generate a <RouterLink :to="`/${$site.themeConfig.typesenseLatestVersion}/api/api-keys.html#search-only-api-key`">search-only API key</RouterLink> and serve it to your app from a backend endpoint, along with your Typesense cluster endpoints. This way you can rotate your API keys and change your cluster endpoints at any point as needed.
Create the types file:
touch types/Book.ts
Add this to types/Book.ts:
export interface Book {
id: string;
title: string;
authors: string[];
image_url: string;
publication_year: number;
average_rating?: number;
ratings_count?: number;
}
Your project structure should now look like this:
typesense-react-native-search-bar/
├── assets/
├── components/
├── types/
│ └── Book.ts
├── utils/
├── .env
├── App.tsx
├── app.json
├── package.json
└── tsconfig.json
Create the Typesense utility file:
touch utils/typesense.ts
Add this to utils/typesense.ts:
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: process.env.EXPO_PUBLIC_TYPESENSE_API_KEY || "xyz",
nodes: [
{
host: process.env.EXPO_PUBLIC_TYPESENSE_HOST || "localhost",
port: Number(process.env.EXPO_PUBLIC_TYPESENSE_PORT) || 8108,
protocol: process.env.EXPO_PUBLIC_TYPESENSE_PROTOCOL || "http",
},
],
},
additionalSearchParameters: {
query_by: "title,authors",
},
});
export const searchClient = typesenseInstantsearchAdapter.searchClient;
This utility file creates the Typesense InstantSearch adapter, which bridges Typesense with InstantSearch. The adapter handles all the communication with Typesense and provides a search client that works seamlessly with react-instantsearch-core hooks.
Create the component files:
touch components/SearchInput.tsx
touch components/BookCard.tsx
touch components/BookList.tsx
Your project structure should now look like this:
typesense-react-native-search-bar/
├── assets/
├── components/
│ ├── BookCard.tsx
│ ├── BookList.tsx
│ └── SearchInput.tsx
├── types/
│ └── Book.ts
├── utils/
│ └── typesense.ts
├── .env
├── App.tsx
├── app.json
├── package.json
└── tsconfig.json
Create the SearchInput component in components/SearchInput.tsx:
import React from "react";
import { StyleSheet, TextInput } from "react-native";
import { useSearchBox } from "react-instantsearch-core";
export const SearchInput = () => {
const { query, refine } = useSearchBox();
return (
<TextInput
style={styles.searchInput}
placeholder="Search books..."
placeholderTextColor="#999"
value={query}
onChangeText={refine}
/>
);
};
const styles = StyleSheet.create({
searchInput: {
backgroundColor: "#444",
color: "white",
padding: 15,
borderRadius: 8,
marginHorizontal: 20,
marginBottom: 20,
marginTop: 0,
fontSize: 16,
},
});
This component uses the useSearchBox hook from react-instantsearch-core to connect the search input to Typesense. The query value and refine function are provided by InstantSearch, which handles debouncing and search state management automatically.
Create the BookCard component in components/BookCard.tsx:
import React from "react";
import { View, Image, Text, StyleSheet, Dimensions } from "react-native";
import { Book } from "../types/Book";
export const BookCard = ({ book }: { book: Book }) => {
return (
<View key={book.id} style={styles.card}>
<Image
source={{ uri: book.image_url }}
style={styles.bookImage}
resizeMode="cover"
/>
<View style={styles.cardContent}>
<Text style={styles.title} numberOfLines={2}>
{book.title}
</Text>
<Text style={styles.authors} numberOfLines={1}>
{book.authors.join(", ")}
</Text>
<Text style={styles.year}>{book.publication_year}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
card: {
backgroundColor: "#333",
borderRadius: 12,
margin: 8,
width: Dimensions.get("window").width / 2 - 24,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 5,
},
bookImage: {
width: "100%",
height: 200,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
},
cardContent: {
padding: 12,
},
title: {
fontSize: 16,
fontWeight: "bold",
color: "white",
marginBottom: 4,
},
authors: {
fontSize: 14,
color: "#ccc",
marginBottom: 4,
},
year: {
fontSize: 12,
color: "#999",
},
});
Create the BookList component in components/BookList.tsx:
import React from "react";
import { useHits } from "react-instantsearch-core";
import { BookCard } from "./BookCard";
import { Book } from "../types/Book";
export const BookList = () => {
const { hits } = useHits<Book>();
return (
<>
{hits.map((book) => (
<BookCard key={book.id} book={book} />
))}
</>
);
};
This component uses the useHits hook from react-instantsearch-core to automatically receive search results from Typesense. The hook provides the hits array which updates in real-time as the user types in the search box.
Finally, update your App.tsx to bring everything together:
import { StatusBar } from "expo-status-bar";
import { StyleSheet, View, ScrollView } from "react-native";
import {
SafeAreaProvider,
useSafeAreaInsets,
} from "react-native-safe-area-context";
import { InstantSearch } from "react-instantsearch-core";
import { Heading } from "./components/Heading";
import { BookList } from "./components/BookList";
import { SearchInput } from "./components/SearchInput";
import { searchClient } from "./utils/typesense";
function AppContent() {
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<ScrollView>
<Heading />
<SearchInput />
<View style={styles.grid}>
<BookList />
</View>
<StatusBar style="auto" />
</ScrollView>
</View>
);
}
export default function App() {
return (
<SafeAreaProvider>
<InstantSearch searchClient={searchClient} indexName="books">
<AppContent />
</InstantSearch>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#222",
},
grid: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "center",
paddingHorizontal: 8,
},
});
This main component wraps the app with the InstantSearch provider, which connects all the search components to Typesense. The searchClient from our utility file and the indexName ("books") are passed to the provider. The SafeAreaProvider ensures the app respects device-specific safe areas like notches and home indicators.
Your final project structure should look like this:
typesense-react-native-search-bar/
├── assets/
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ └── splash-icon.png
├── components/
│ ├── BookCard.tsx
│ ├── BookList.tsx
│ └── SearchInput.tsx
├── types/
│ └── Book.ts
├── utils/
│ └── typesense.ts
├── .env
├── App.tsx
├── app.json
├── package.json
└── tsconfig.json
Run the application:
npx expo start
This will start the Expo development server. You can then:
a to open in Android emulator.i to open in iOS simulator.w to open in web browser.You've successfully built a mobile search interface with React Native and Typesense!
Here's how the final output should look like on a mobile device:
<div style="text-align: center; margin: 20px 0;"> </div>Here's the complete source code for this project on GitHub:
https://github.com/typesense/code-samples/tree/master/typesense-react-native-search-bar
Here are other related examples that show you how to build search interfaces with different frameworks:
Read our Help section for information on how to get additional help.