| name | react-native-patterns |
| description | React Native / Expo patterns for Frappe-backed mobile apps. Reference for Axios API calls, React Query v4, Redux Toolkit, React Navigation, expo-location, NetworkContext, transport journey lifecycle, attendance flow, and EAS builds. |
React Native Patterns for Frappe Mobile Apps
Quick reference for building Expo React Native apps that integrate with a Frappe backend.
App Overview
Location: chatnext-mobile-old/
Stack: React Native 0.71 + Expo ~48 + React Navigation v6 + Redux Toolkit + TanStack React Query v4 + Axios
Frappe API Integration
All API calls go through the pre-configured Axios instance:
import apiClient from '../utils/apiClient';
const res = await apiClient.post(
'/api/method/transporter.transporter.api.transport.create_journey',
{ vehicle, route, route_stops: JSON.stringify(stops) }
);
const result = res.data.message;
const res = await apiClient.get('/api/resource/Route List?fields=["*"]');
const routes = res.data.data;
Auth headers (site name + CSRF token) are injected automatically by the request interceptor in src/utils/apiClient.ts.
Canonical Transport Endpoints
# Always use transporter app — NEVER edu_quality.api.transport.*
POST /api/method/transporter.transporter.api.transport.create_journey
POST /api/method/transporter.transporter.api.transport.add_gps_log
POST /api/method/transporter.transporter.api.transport.end_journey_api
GET /api/method/transporter.transporter.api.transport.get_active_journey
POST /api/method/transporter.transporter.api.transport.cleanup_room_silent
# Attendance
POST /api/method/edu_quality.edu_quality.server_scripts.student.mark_entry
# Routes
GET /api/method/edu_quality.api.route_list.get_route_list_with_stops
GET /api/method/edu_quality.api.route_list.get_route_student_attendance
React Query v4
Important: v4 uses positional arguments — NOT the v5 object syntax.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
const { data, isLoading, refetch } = useQuery(
['route-list', schoolId],
() => apiClient.get('/api/resource/...').then(r => r.data.data),
{
staleTime: 5 * 60 * 1000,
enabled: !!schoolId,
retry: 2,
}
);
const queryClient = useQueryClient();
const { mutate, isLoading: isSaving } = useMutation(
(params: CreateParams) =>
apiClient.post('/api/method/...', params).then(r => r.data.message),
{
onSuccess: () => queryClient.invalidateQueries(['route-list']),
onError: (err) => console.error('Failed:', err),
}
);
Redux Toolkit
import { useSelector, useDispatch } from 'react-redux';
import type { RootState } from '../store';
const baseUrl = useSelector((s: RootState) => s.path.baseURL);
const dispatch = useDispatch();
import { setBaseURL } from '../store/pathSlice';
dispatch(setBaseURL('https://school.example.com'));
React Navigation
import { useNavigation, useRoute } from '@react-navigation/native';
const navigation = useNavigation();
const route = useRoute();
navigation.navigate('LocationTracker');
navigation.navigate('AttendanceScreen', { routeId: 'ROUTE-001' });
navigation.goBack();
<Stack.Screen name="NewScreen" component={NewScreenComponent} />
Network Awareness
import { useNetwork } from '../contexts/NetworkContext';
const { isConnected, isInternetReachable, onConnectionRestored } = useNetwork();
if (!isConnected || !isInternetReachable) {
return;
}
useEffect(() => {
return onConnectionRestored(() => {
refetch();
});
}, []);
Location (GPS)
import * as Location from 'expo-location';
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return;
const loc = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
const { latitude, longitude } = loc.coords;
const sub = await Location.watchPositionAsync(
{ accuracy: Location.Accuracy.High, timeInterval: 5000, distanceInterval: 10 },
(loc) => {
const { latitude, longitude } = loc.coords;
if (isNaN(latitude) || isNaN(longitude)) return;
}
);
Transport Journey Lifecycle
Boot
└─ TransportDashboard.tsx
└─ fetchActiveJourney() → if found, setIsStartJourney(true) [crash recovery]
Start Journey
└─ LocationTracker.tsx → useTransportApi.createJourney(vehicle, route, stops)
→ returns { journey_id, websocket_room, status: "success" | "resumed" }
GPS Logging (every 30s)
└─ useTransportApi.sendGpsLog({ currentLocation, optimisedRoute, current_stop_index, roomID })
Attendance
└─ AttendanceScreen.tsx → mark_entry(student, status, reason)
reason = "Pickup - Double-tap onboard" | "Drop - Double-tap onboard"
End Journey
└─ useTransportApi.endJourney(journeyId, vehicle, route)
Attendance Marking
const reason = `${isPickup ? 'Pickup' : 'Drop'} - Double-tap onboard`;
await apiClient.post(
'/api/method/edu_quality.edu_quality.server_scripts.student.mark_entry',
{
student: studentId,
status: isPickup ? 'Present' : 'Present',
reason,
date: format(new Date(), 'yyyy-MM-dd'),
time: format(new Date(), 'HH:mm:ss'),
}
);
AsyncStorage (Persistence)
import AsyncStorage from '@react-native-async-storage/async-storage';
await AsyncStorage.setItem('journey_id', journeyId);
const journeyId = await AsyncStorage.getItem('journey_id');
await AsyncStorage.removeItem('journey_id');
Push Notifications
import * as Notifications from 'expo-notifications';
const { data: token } = await Notifications.getExpoPushTokenAsync();
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
Build Commands
cd chatnext-mobile-old
npx expo start --dev-client
npx expo run:android
npx expo run:ios
eas build --platform android --profile preview
eas build --platform android --profile production
eas build --platform ios --profile production
Coordinate Validation
Always validate GPS coordinates before sending to the server:
function isValidCoord(lat: number, lng: number): boolean {
return (
!isNaN(lat) && !isNaN(lng) &&
lat >= -90 && lat <= 90 &&
lng >= -180 && lng <= 180 &&
lat !== 0 && lng !== 0
);
}