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-storageCreate 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 };
}Related
Last updated on