Skip to Content
APSO is 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