Learn how to use Flowfull Client in your React Native and Expo apps.
# npm
npm install @pubflow/flowfull-client
# yarn
yarn add @pubflow/flowfull-client
# expo
npx expo install @pubflow/flowfull-client// api/client.ts
import { createFlowfull } from '@pubflow/flowfull-client';
import Constants from 'expo-constants';
export const api = createFlowfull(
Constants.expoConfig?.extra?.apiUrl || 'https://api.myapp.com'
);// app.config.js
export default {
expo: {
extra: {
apiUrl: process.env.EXPO_PUBLIC_API_URL || 'https://api.myapp.com'
}
}
};Flowfull Client automatically detects sessions from AsyncStorage:
import { createFlowfull } from '@pubflow/flowfull-client';
// Automatically looks for 'pubflow_session_id' in AsyncStorage
const api = createFlowfull('https://api.myapp.com');import AsyncStorage from '@react-native-async-storage/async-storage';
const api = createFlowfull('https://api.myapp.com', {
getSessionId: async () => {
return await AsyncStorage.getItem('session_id');
}
});// screens/LoginScreen.tsx
import { api } from '../api/client';
import AsyncStorage from '@react-native-async-storage/async-storage';
async function handleLogin(email: string, password: string) {
const response = await api.post('/auth/login', { email, password });
if (response.success && response.data.session_id) {
// Save session
await AsyncStorage.setItem('pubflow_session_id', response.data.session_id);
// Update client
api.setSessionId(response.data.session_id);
// Navigate to home
navigation.navigate('Home');
} else {
Alert.alert('Error', response.error || 'Login failed');
}
}async function handleLogout() {
// Call logout endpoint
await api.post('/auth/logout');
// Clear session from AsyncStorage
await AsyncStorage.removeItem('pubflow_session_id');
await AsyncStorage.removeItem('pubflow_user_data');
// Clear session from client
api.clearSession();
// Navigate to login
navigation.navigate('Login');
}import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
import { api } from '../api/client';
interface User {
id: string;
name: string;
email: string;
}
export function UsersScreen() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadUsers();
}, []);
async function loadUsers() {
setLoading(true);
setError(null);
const response = await api
.query('/users')
.where('status', 'active')
.sort('name', 'asc')
.limit(50)
.get<User[]>();
if (response.success) {
setUsers(response.data || []);
} else {
setError(response.error || 'Failed to load users');
}
setLoading(false);
}
if (loading) {
return <ActivityIndicator size="large" />;
}
if (error) {
return <Text>Error: {error}</Text>;
}
return (
<FlatList
data={users}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>{item.email}</Text>
</View>
)}
/>
);
}import React, { useEffect, useState } from 'react';
import { FlatList, ActivityIndicator } from 'react-native';
import { api } from '../api/client';
export function ProductsScreen() {
const [products, setProducts] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
async function loadProducts() {
if (loading || !hasMore) return;
setLoading(true);
const response = await api
.query('/products')
.where('status', 'active')
.page(page)
.limit(20)
.get();
if (response.success) {
setProducts([...products, ...response.data]);
setHasMore(response.meta?.hasMore || false);
setPage(page + 1);
}
setLoading(false);
}
useEffect(() => {
loadProducts();
}, []);
return (
<FlatList
data={products}
onEndReached={loadProducts}
onEndReachedThreshold={0.5}
ListFooterComponent={loading ? <ActivityIndicator /> : null}
/>
);
}import React, { useState, useEffect } from 'react';
import { View, TextInput, FlatList } from 'react-native';
import { api } from '../api/client';
export function SearchScreen() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
if (searchTerm.length >= 3) {
performSearch();
} else {
setResults([]);
}
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
async function performSearch() {
setLoading(true);
const response = await api
.query('/products')
.search(searchTerm)
.where('status', 'active')
.limit(20)
.get();
if (response.success) {
setResults(response.data || []);
}
setLoading(false);
}
return (
<View>
<TextInput
placeholder="Search products..."
value={searchTerm}
onChangeText={setSearchTerm}
/>
<FlatList
data={results}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ProductItem product={item} />}
/>
</View>
);
}import React, { useState } from 'react';
import { View, TextInput, Button, Alert } from 'react-native';
import { api } from '../api/client';
export function CreateUserScreen({ navigation }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit() {
setLoading(true);
const response = await api.post('/users', {
name,
email
});
setLoading(false);
if (response.success) {
Alert.alert('Success', 'User created successfully');
navigation.goBack();
} else {
Alert.alert('Error', response.error || 'Failed to create user');
}
}
return (
<View>
<TextInput
placeholder="Name"
value={name}
onChangeText={setName}
/>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<Button
title="Create User"
onPress={handleSubmit}
disabled={loading}
/>
</View>
);
}Create a reusable hook for API calls:
// hooks/useApi.ts
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import type { ApiResponse } from '@pubflow/flowfull-client';
export function useApi<T>(
endpoint: string,
options?: {
autoLoad?: boolean;
filters?: Record<string, any>;
}
) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function load() {
setLoading(true);
setError(null);
let query = api.query(endpoint);
if (options?.filters) {
Object.entries(options.filters).forEach(([key, value]) => {
query = query.where(key, value);
});
}
const response = await query.get<T>();
if (response.success) {
setData(response.data || null);
} else {
setError(response.error || 'Request failed');
}
setLoading(false);
return response;
}
useEffect(() => {
if (options?.autoLoad !== false) {
load();
}
}, []);
return { data, loading, error, reload: load };
}
// Usage
export function UsersScreen() {
const { data: users, loading, error, reload } = useApi<User[]>('/users', {
filters: { status: 'active' }
});
if (loading) return <ActivityIndicator />;
if (error) return <Text>Error: {error}</Text>;
return <UserList users={users || []} onRefresh={reload} />;
}- Create a single API instance and export it
- Use AsyncStorage for session persistence
- Handle loading states in your UI
- Show error messages to users
- Implement pull-to-refresh for better UX
- Use custom hooks for reusable API logic
- Debounce search inputs to reduce API calls
- Clear session on logout from both AsyncStorage and client
Enable logging in development:
import { createFlowfull } from '@pubflow/flowfull-client';
const api = createFlowfull('https://api.myapp.com');
if (__DEV__) {
api.addRequestInterceptor(async (config) => {
console.log('📤 Request:', config.method, config.url);
return config;
});
api.addResponseInterceptor(async (response) => {
console.log('📥 Response:', response.status, response.success);
return response;
});
}Next: TypeScript Guide