react-spa-patterns
React SPA Patterns for Frappe
Quick reference for building React SPAs that integrate with a Frappe backend.
Project Overview
| App | Path | Stack |
|---|---|---|
| Unity Parent App | apps/unity_parent_app/new_frontend/ |
React 18 + Vite 6 + shadcn/ui + Tailwind + TanStack Query v5 + Jotai + Axios |
| Walsh Admin Portal | apps/edu_quality/walsh/ |
React 18 + Vite 5 + Refine v4 + Mantine v5 + React Query v3 |
Frappe API Integration
Axios (Parent App)
// src/utils/axiosInstance.ts — pre-configured with withCredentials: true
import axiosInstance from '@/utils/axiosInstance';
// Call a whitelisted method
const res = await axiosInstance.post(
'/api/method/app.module.function',
{ student_id: 'STU-001' }
);
const data = res.data.message; // Frappe wraps in .message
// Fetch a resource
const res = await axiosInstance.get('/api/resource/Student/STU-001');
const student = res.data.data;
Refine Data Provider (Walsh)
import { useList, useOne, useCreate, useUpdate, useDelete } from "@refinedev/core";
// List
const { data } = useList({
resource: "Student",
filters: [{ field: "enabled", operator: "eq", value: 1 }],
pagination: { pageSize: 50 },
meta: { dataProviderName: "default" }, // or "notices", "cmap"
});
// data.data = array of records
// Create
const { mutate: create } = useCreate();
create({ resource: "Student", values: { ... } });
// Custom method (not REST)
const res = await dataProvider.custom({
url: '/api/method/edu_quality.api.module.function',
method: 'post',
payload: { param: value },
});
TanStack Query Patterns
Parent App (v5)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Query
const { data, isLoading, error } = useQuery({
queryKey: ['students', academicYear],
queryFn: () =>
axiosInstance.post('/api/method/...', { academic_year: academicYear })
.then(r => r.data.message),
staleTime: 5 * 60 * 1000,
enabled: !!academicYear,
});
// Mutation with cache invalidation
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: (data) => axiosInstance.post('/api/method/...', data).then(r => r.data.message),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['students'] }),
});
Walsh (v3)
import { useQuery, useMutation } from 'react-query';
const { data } = useQuery(
['key', dep],
() => fetch('/api/method/...').then(r => r.json()).then(r => r.message),
{ staleTime: 60_000 }
);
State Management
Jotai (Parent App)
// Define atom
import { atom } from 'jotai';
export const activeStudentAtom = atom<string | null>(null);
// Read + write in component
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
const [student, setStudent] = useAtom(activeStudentAtom);
const student = useAtomValue(activeStudentAtom); // read-only
const setStudent = useSetAtom(activeStudentAtom); // write-only
Routing
React Router v6 (Parent App)
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
<BrowserRouter basename="/parent-app">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/transport" element={<Transport />} />
</Routes>
</BrowserRouter>
// Navigation in component
import { useNavigate, useParams } from 'react-router-dom';
const navigate = useNavigate();
navigate('/transport?active_student=STU-001');
Refine Router (Walsh)
// Routes are driven by Refine resources + react-router-v6 integration
// Add to resources array in <Refine> component
resources={[
{
name: "notices",
list: "/notices",
create: "/notices/create",
edit: "/notices/edit/:id",
meta: { dataProviderName: "notices" },
},
]}
UI Components
shadcn/ui + Tailwind (Parent App)
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { cn } from "@/lib/utils"; // clsx + tailwind-merge
// Variants with CVA
import { cva } from "class-variance-authority";
const buttonVariants = cva("base-classes", {
variants: { size: { sm: "...", lg: "..." } },
});
Mantine v5 (Walsh)
import { Button, TextInput, Select, Stack, Group, Modal } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
const [opened, { open, close }] = useDisclosure(false);
notifications.show({ title: "Saved", message: "Record updated", color: "green" });
Real-Time (Parent App Only)
// src/utils/frappeSocket.ts
import { subscribeToRoom, onRealtimeEvent, offRealtimeEvent, unsubscribeFromRoom } from '@/utils/frappeSocket';
// Subscribe to a room
subscribeToRoom(`transport_journey_${journeyId}`);
// Listen for events
const handler = (data: unknown) => { /* handle */ };
onRealtimeEvent('transport_gps_update', handler);
// Cleanup
offRealtimeEvent('transport_gps_update', handler);
unsubscribeFromRoom(`transport_journey_${journeyId}`);
Build & Deploy
# Parent App
cd apps/unity_parent_app/new_frontend
npm run dev # Dev server :8080
npm run build # Build → public/new_frontend/ + copy www/parent-app.html
# Walsh
cd apps/edu_quality/walsh
yarn dev # Dev server :8080
yarn build # TS compile → Vite build → public/walsh/ + copy www/walsh.html
# After any build
bench --site <site> clear-cache
bench build --app <app> # Only needed if static assets changed
Common Patterns
Protected Route (Parent App)
import { Navigate } from 'react-router-dom';
import { isLoggedIn } from '@/utils/cookies';
const ProtectedRoute = ({ children }) =>
isLoggedIn() ? children : <Navigate to="/login" replace />;
Error Boundary
// Wrap pages in ErrorBoundary from src/components/
import ErrorBoundary from '@/components/ErrorBoundary';
<ErrorBoundary fallback={<ErrorPage />}>
<MyPage />
</ErrorBoundary>
Frappe File Upload
const formData = new FormData();
formData.append('file', file);
formData.append('doctype', 'Student');
formData.append('docname', studentId);
const res = await axiosInstance.post('/api/method/upload_file', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
More from unityappsuite/frappe-claude
bench-commands
Frappe Bench CLI command reference for site management, app management, development, and production operations. Use when running bench commands, managing sites, migrations, builds, or deployments.
23frappe-api
Frappe Python and JavaScript API reference including document operations, database queries, utilities, and REST API patterns. Use when working with frappe.get_doc, frappe.db, frappe.call, or any Frappe API methods.
16client-scripts
Frappe client-side JavaScript patterns for form events, field manipulation, dialogs, and UI customization. Use when writing form scripts, handling field changes, creating dialogs, or customizing the Frappe desk interface.
11doctype-patterns
Frappe DocType creation patterns, field types, controller hooks, and data modeling best practices. Use when creating DocTypes, designing data models, adding fields, or setting up document relationships in Frappe/ERPNext.
10server-scripts
Frappe server-side Python patterns for controllers, document events, whitelisted APIs, background jobs, and database operations. Use when writing controller logic, creating APIs, handling document events, or processing data on the server.
10react-native-patterns
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.
2