react-spa-patterns

Installation
SKILL.md

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' },
});
Related skills
Installs
2
GitHub Stars
16
First Seen
Apr 19, 2026