Skip to Content
🚀 APSO is now in public beta. Get started →
GuidesFrontendReact Native

React Native Integration

Build mobile applications with React Native connected to your APSO backend.

Setup

Install Dependencies

npm install @apso/sdk npm install @react-native-async-storage/async-storage

Create Client

// src/lib/apso.ts import { createClient } from '@apso/sdk'; import AsyncStorage from '@react-native-async-storage/async-storage'; export const apso = createClient({ baseUrl: 'https://api.yourapp.com', }); // Token management export const initializeAuth = async () => { const token = await AsyncStorage.getItem('token'); if (token) { apso.setToken(token); } }; export const setAuthToken = async (token: string) => { await AsyncStorage.setItem('token', token); apso.setToken(token); }; export const clearAuthToken = async () => { await AsyncStorage.removeItem('token'); apso.setToken(null); };

Authentication

Auth Context

// src/contexts/AuthContext.tsx import React, { createContext, useContext, useState, useEffect } from 'react'; import { apso, initializeAuth, setAuthToken, clearAuthToken } from '../lib/apso'; interface User { id: string; email: string; name: string; } interface AuthContextType { user: User | null; loading: boolean; login: (email: string, password: string) => Promise<void>; logout: () => Promise<void>; } const AuthContext = createContext<AuthContextType | null>(null); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { const init = async () => { await initializeAuth(); try { const me = await apso.auth.me(); setUser(me); } catch { // Not authenticated } setLoading(false); }; init(); }, []); const login = async (email: string, password: string) => { const { token, user } = await apso.auth.login({ email, password }); await setAuthToken(token); setUser(user); }; const logout = async () => { await clearAuthToken(); setUser(null); }; return ( <AuthContext.Provider value={{ user, loading, login, logout }}> {children} </AuthContext.Provider> ); } export const useAuth = () => { const context = useContext(AuthContext); if (!context) throw new Error('useAuth must be used within AuthProvider'); return context; };

Custom Hooks

useQuery Hook

// src/hooks/useQuery.ts import { useState, useEffect, useCallback } from 'react'; export function useQuery<T>( queryFn: () => Promise<T>, deps: unknown[] = [] ) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const refetch = useCallback(async () => { setLoading(true); try { const result = await queryFn(); setData(result); setError(null); } catch (e) { setError(e as Error); } setLoading(false); }, deps); useEffect(() => { refetch(); }, [refetch]); return { data, loading, error, refetch }; }

Screen Examples

List Screen

// src/screens/ProjectsScreen.tsx import React from 'react'; import { View, Text, FlatList, TouchableOpacity, ActivityIndicator } from 'react-native'; import { useQuery } from '../hooks/useQuery'; import { apso } from '../lib/apso'; export function ProjectsScreen({ navigation }) { const { data: projects, loading, error, refetch } = useQuery( () => apso.projects.findMany({ orderBy: { createdAt: 'desc' } }), [] ); if (loading) { return ( <View style={styles.center}> <ActivityIndicator size="large" /> </View> ); } if (error) { return ( <View style={styles.center}> <Text>Error: {error.message}</Text> <TouchableOpacity onPress={refetch}> <Text>Retry</Text> </TouchableOpacity> </View> ); } return ( <FlatList data={projects} keyExtractor={item => item.id} onRefresh={refetch} refreshing={loading} renderItem={({ item }) => ( <TouchableOpacity onPress={() => navigation.navigate('ProjectDetail', { id: item.id })} > <View style={styles.item}> <Text style={styles.title}>{item.name}</Text> <Text style={styles.description}>{item.description}</Text> </View> </TouchableOpacity> )} /> ); }

Detail Screen

// src/screens/ProjectDetailScreen.tsx import React from 'react'; import { View, Text, ScrollView, ActivityIndicator } from 'react-native'; import { useQuery } from '../hooks/useQuery'; import { apso } from '../lib/apso'; export function ProjectDetailScreen({ route }) { const { id } = route.params; const { data: project, loading } = useQuery( () => apso.projects.findUnique({ where: { id }, include: { tasks: true }, }), [id] ); if (loading || !project) { return <ActivityIndicator size="large" />; } return ( <ScrollView style={styles.container}> <Text style={styles.title}>{project.name}</Text> <Text style={styles.description}>{project.description}</Text> <Text style={styles.sectionTitle}>Tasks ({project.tasks.length})</Text> {project.tasks.map(task => ( <View key={task.id} style={styles.taskItem}> <Text>{task.title}</Text> <Text style={styles.status}>{task.status}</Text> </View> ))} </ScrollView> ); }

Create Screen

// src/screens/CreateProjectScreen.tsx import React, { useState } from 'react'; import { View, TextInput, TouchableOpacity, Text, Alert } from 'react-native'; import { apso } from '../lib/apso'; export function CreateProjectScreen({ navigation }) { const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async () => { if (!name.trim()) { Alert.alert('Error', 'Name is required'); return; } setLoading(true); try { await apso.projects.create({ name: name.trim(), description: description.trim() || undefined, }); navigation.goBack(); } catch (error) { Alert.alert('Error', error.message); } setLoading(false); }; return ( <View style={styles.container}> <TextInput style={styles.input} value={name} onChangeText={setName} placeholder="Project name" /> <TextInput style={[styles.input, styles.textArea]} value={description} onChangeText={setDescription} placeholder="Description" multiline /> <TouchableOpacity style={styles.button} onPress={handleSubmit} disabled={loading} > <Text style={styles.buttonText}> {loading ? 'Creating...' : 'Create Project'} </Text> </TouchableOpacity> </View> ); }

Offline Support

For offline-first functionality:

// src/lib/offlineStorage.ts import AsyncStorage from '@react-native-async-storage/async-storage'; export const cacheData = async (key: string, data: unknown) => { await AsyncStorage.setItem(`cache:${key}`, JSON.stringify(data)); }; export const getCachedData = async <T>(key: string): Promise<T | null> => { const cached = await AsyncStorage.getItem(`cache:${key}`); return cached ? JSON.parse(cached) : null; };
// src/hooks/useOfflineQuery.ts import { useState, useEffect } from 'react'; import NetInfo from '@react-native-community/netinfo'; import { cacheData, getCachedData } from '../lib/offlineStorage'; export function useOfflineQuery<T>( key: string, queryFn: () => Promise<T> ) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetch = async () => { // Try cache first const cached = await getCachedData<T>(key); if (cached) setData(cached); // Check network const { isConnected } = await NetInfo.fetch(); if (isConnected) { try { const fresh = await queryFn(); setData(fresh); await cacheData(key, fresh); } catch (e) { if (!cached) throw e; } } setLoading(false); }; fetch(); }, [key]); return { data, loading }; }
Last updated on