Arcanea React Best Practices
"Leyla guards the Gate of Flow at 285 Hz. Your components must flow, not block. Every waterfall is a Flow violation."
Adapted from Vercel Engineering's 57-rule guide for the Arcanea stack: Next.js 16 App Router + React 19 + TypeScript strict + Supabase + Vercel AI SDK 6.
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Eliminating Waterfalls | CRITICAL | async- |
| 2 | Bundle Size | CRITICAL | bundle- |
| 3 | Server-Side | HIGH | server- |
| 4 | Client Data Fetching | MEDIUM-HIGH | client- |
| 5 | Re-render Optimization | MEDIUM | rerender- |
| 6 | Rendering Performance | MEDIUM | rendering- |
| 7 | JavaScript Performance | LOW-MEDIUM | js- |
| 8 | Advanced Patterns | LOW | advanced- |
1. Eliminating Waterfalls (CRITICAL)
async-parallel — Use Promise.all for independent fetches
typescript1// BAD — serial waterfall 2const user = await getUser(id) 3const posts = await getUserPosts(id) // waits for user unnecessarily 4 5// GOOD — parallel 6const [user, posts] = await Promise.all([getUser(id), getUserPosts(id)])
async-defer-await — Move await to where it's actually needed
typescript1// BAD 2async function handler() { 3 const data = await fetchData() // awaited before branch 4 if (condition) return data 5 return null 6} 7 8// GOOD 9async function handler() { 10 const dataPromise = fetchData() // start early 11 if (!condition) return null 12 return await dataPromise // await only when needed 13}
async-suspense-boundaries — Stream content with Suspense
tsx1// In Arcanea pages — wrap slow data in Suspense for progressive rendering 2export default function LorePage() { 3 return ( 4 <Suspense fallback={<GlassCardSkeleton />}> 5 <LoreContent /> {/* streams in independently */} 6 </Suspense> 7 ) 8}
async-api-routes — Start promises early in route handlers
typescript1// app/api/guardians/route.ts 2export async function GET() { 3 const guardianPromise = supabase.from('guardians').select() // start immediately 4 // ... validation/auth work ... 5 const { data } = await guardianPromise // await late 6 return Response.json(data) 7}
2. Bundle Size (CRITICAL)
bundle-barrel-imports — Import directly, never from barrel files
typescript1// BAD — pulls in entire barrel 2import { Button, Card, Modal } from '@/components/ui' 3 4// GOOD — direct imports 5import { Button } from '@/components/ui/button' 6import { Card } from '@/components/ui/card'
bundle-dynamic-imports — Lazy-load heavy components
typescript1// Heavy Arcanea components: GlobeMap, PromptEditor, LoreCanvas 2import dynamic from 'next/dynamic' 3 4const LoreCanvas = dynamic(() => import('@/components/lore/LoreCanvas'), { 5 loading: () => <GlassCardSkeleton />, 6 ssr: false, // client-only canvas 7})
bundle-defer-third-party — Analytics after hydration
typescript1// In Arcanea layout — defer non-critical scripts 2import { GoogleAnalytics } from '@next/third-parties/google' 3// Renders after hydration automatically via next/third-parties
3. Server-Side Performance (HIGH)
server-cache-react — Deduplicate DB calls per request
typescript1// lib/supabase-cached.ts — use React.cache for per-request dedup 2import { cache } from 'react' 3import { supabase } from '@/lib/supabase' 4 5export const getGuardian = cache(async (gateName: string) => { 6 const { data } = await supabase 7 .from('guardians') 8 .select() 9 .eq('gate', gateName) 10 .single() 11 return data 12}) 13// Multiple RSCs can call getGuardian('Fire') — only one DB query
server-auth-actions — Always authenticate Server Actions
typescript1// app/actions/prompt.ts 2'use server' 3import { createServerClient } from '@/lib/supabase-server' 4 5export async function savePrompt(data: PromptData) { 6 const supabase = createServerClient() 7 const { data: { user } } = await supabase.auth.getUser() 8 if (!user) throw new Error('Unauthorized') // NEVER skip this 9 // ... 10}
server-parallel-fetching — Restructure RSC to parallelize
typescript1// BAD — sequential RSC 2async function GuardianPage({ gate }: { gate: string }) { 3 const guardian = await getGuardian(gate) // serial 4 const godbeast = await getGodbeast(guardian.id) // waits 5} 6 7// GOOD — parallel RSC 8async function GuardianPage({ gate }: { gate: string }) { 9 const [guardian, godbeast] = await Promise.all([ 10 getGuardian(gate), 11 getGodBeastByGate(gate), // use gate directly 12 ]) 13}
4. Client Data Fetching (MEDIUM-HIGH)
client-swr-dedup — SWR/React Query for client fetches
typescript1// Prefer SWR for client-side Arcanea data 2import useSWR from 'swr' 3 4function useGuardians() { 5 return useSWR('/api/guardians', fetcher, { 6 revalidateOnFocus: false, // lore data doesn't change often 7 }) 8}
5. Re-render Optimization (MEDIUM)
rerender-memo — Memoize expensive Arcanea components
typescript1// Heavy visual components: GuardianCard, LoreGrid, GodBeastProfile 2import { memo } from 'react' 3 4const GuardianCard = memo(function GuardianCard({ guardian }: Props) { 5 return <div className="glass-card">...</div> 6})
rerender-derived-state-no-effect — Derive during render
typescript1// BAD — unnecessary effect 2const [filteredGuardians, setFiltered] = useState([]) 3useEffect(() => { 4 setFiltered(guardians.filter(g => g.element === selectedElement)) 5}, [guardians, selectedElement]) 6 7// GOOD — derive during render 8const filteredGuardians = guardians.filter(g => g.element === selectedElement)
rerender-lazy-state-init — Lazy init for expensive state
typescript1// BAD — recalculates every render 2const [state, setState] = useState(computeExpensiveDefault()) 3 4// GOOD — only runs once 5const [state, setState] = useState(() => computeExpensiveDefault())
rerender-transitions — Non-urgent updates via startTransition
typescript1import { useTransition } from 'react' 2 3function GateFilter() { 4 const [isPending, startTransition] = useTransition() 5 6 return ( 7 <select onChange={e => startTransition(() => setGate(e.target.value))}> 8 {/* Gate filter won't block urgent UI updates */} 9 </select> 10 ) 11}
6. Rendering Performance (MEDIUM)
rendering-hoist-jsx — Extract static JSX outside components
typescript1// BAD — recreated every render 2function GuardianList() { 3 const header = <h2 className="text-gradient-aurora">The Ten Gates</h2> 4 return <div>{header}{items}</div> 5} 6 7// GOOD — hoisted static 8const HEADER = <h2 className="text-gradient-aurora">The Ten Gates</h2> 9function GuardianList() { 10 return <div>{HEADER}{items}</div> 11}
rendering-conditional-render — Ternary, not &&
typescript1// BAD — renders "0" if count is 0 2{count && <Badge>{count}</Badge>} 3 4// GOOD 5{count > 0 ? <Badge>{count}</Badge> : null}
7. JavaScript Performance (LOW-MEDIUM)
js-index-maps — Map for repeated lookups
typescript1// BAD — O(n) per lookup 2function getGuardian(gate: string) { 3 return guardians.find(g => g.gate === gate) 4} 5 6// GOOD — O(1) lookup 7const guardianMap = new Map(guardians.map(g => [g.gate, g])) 8const getGuardian = (gate: string) => guardianMap.get(gate)
js-early-exit — Return early from functions
typescript1// GOOD Arcanea pattern 2function validateGateUnlock(user: User, gate: Gate): ValidationResult { 3 if (!user.authenticated) return { valid: false, reason: 'unauthenticated' } 4 if (user.gatesOpen < gate.requiredRank) return { valid: false, reason: 'rank' } 5 if (gate.locked) return { valid: false, reason: 'sealed' } 6 return { valid: true } 7}
8. Arcanea-Specific Patterns
AI SDK 6 — Vercel AI SDK streaming
typescript1// app/api/guardian-chat/route.ts — AI SDK 6 pattern 2import { streamText } from 'ai' 3import { google } from '@ai-sdk/google' 4 5export async function POST(req: Request) { 6 const { messages } = await req.json() 7 const result = streamText({ 8 model: google('gemini-2.5-flash'), 9 messages, 10 maxOutputTokens: 2048, // NOT maxTokens (SDK 6 change) 11 }) 12 return result.toUIMessageStreamResponse() // NOT toDataStreamResponse (SDK 6) 13}
Supabase Auth in Server Components
typescript1// Always use server-side auth for SSR 2import { createServerClient } from '@supabase/ssr' 3import { cookies } from 'next/headers' 4 5export async function getServerUser() { 6 const cookieStore = cookies() 7 const supabase = createServerClient( 8 process.env.NEXT_PUBLIC_SUPABASE_URL!, 9 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 10 { cookies: { get: (name) => cookieStore.get(name)?.value } } 11 ) 12 const { data: { user } } = await supabase.auth.getUser() 13 return user 14}
Glass Components — Design System compliance
typescript1// Always use Arcanean Design System classes 2// See: .arcanea/design/DESIGN_BIBLE.md 3 4// Glass tiers: glass-subtle (8px) | glass (16px) | glass-strong (24px) | liquid-glass (40px) 5// Text: text-gradient-aurora | text-gradient-gold | text-gradient-violet 6// Animations: float | pulse-glow | shimmer | cosmic-drift 7// FONTS: Inter everywhere (NO Cinzel in new code — MEMORY.md override) 8 9function GuardianCard({ guardian }: Props) { 10 return ( 11 <div className="glass hover-lift glow-card"> 12 <h3 className="text-gradient-aurora">{guardian.name}</h3> 13 </div> 14 ) 15}
Quick Checklist
Before any React/Next.js PR in Arcanea:
- No sequential awaits where Promise.all applies
- Heavy components use
next/dynamic - No barrel imports — direct imports only
- Server Actions authenticate with Supabase before any mutation
-
React.cache()wraps repeated DB calls in same RSC tree - State derived during render, not in useEffect
- Vercel AI SDK 6:
maxOutputTokensandtoUIMessageStreamResponse - Glass components use Design Bible classes
- Fonts: Inter only (no Cinzel in new code)