Awesome-omni-skill frontend-patterns
Next.js 16 frontend best practices for Splits Network portal and candidate apps
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/frontend-patterns-splits-network" ~/.claude/skills/diegosouzapw-awesome-omni-skill-frontend-patterns-e1150a && rm -rf "$T"
skills/development/frontend-patterns-splits-network/SKILL.mdFrontend Patterns Skill
This skill provides guidance for building consistent, performant frontend applications using Next.js 16 App Router.
Purpose
Help developers create high-quality frontend code following Splits Network standards:
- Progressive Loading: Load critical data first, lazy load secondary data
- API Integration: Use shared-api-client with proper error handling
- State Management: Client-side state patterns
- Component Architecture: Server vs Client Components
- UI Patterns: DaisyUI components and Tailwind utilities
When to Use This Skill
Use this skill when:
- Creating new pages or components
- Implementing data fetching patterns
- Building forms or interactive UI
- Optimizing page performance
- Handling errors and loading states
Core Principles
1. Progressive Loading Pattern ⚡
CRITICAL FOR PERFORMANCE: Never block page render waiting for all data.
// ✅ CORRECT - Load critical data first, secondary data in parallel const [job, setJob] = useState(null); const [loading, setLoading] = useState(true); const [applications, setApplications] = useState([]); const [applicationsLoading, setApplicationsLoading] = useState(true); // Load primary data immediately useEffect(() => { async function loadJob() { const res = await client.get(`/jobs/${id}`); setJob(res.data); setLoading(false); } loadJob(); }, [id]); // Load secondary data after primary loads useEffect(() => { async function loadApplications() { const res = await client.get(`/applications?job_id=${id}`); setApplications(res.data); setApplicationsLoading(false); } if (job) loadApplications(); }, [job]); // ❌ WRONG - Loading all data in one effect blocks entire page useEffect(() => { async function loadAll() { const job = await client.get(`/jobs/${id}`); const applications = await client.get(`/applications?job_id=${id}`); // Page is blank for 2-3 seconds } loadAll(); }, []);
See examples/progressive-loading.tsx for complete implementation.
2. API Client Usage
Use
@splits-network/shared-api-client for all API calls:
import { createApiClient } from '@splits-network/shared-api-client'; const client = createApiClient(); // Shared-api-client automatically prepends /api/v2 const { data } = await client.get('/jobs'); // Calls /api/v2/jobs // Handle errors try { const { data } = await client.post('/jobs', jobData); setJob(data); } catch (error) { setError(error.message || 'Failed to create job'); }
Important: Routes use
/api/v2 prefix automatically. Frontend calls simple paths like /jobs, not /api/v2/jobs.
See examples/api-client-usage.tsx for patterns.
3. Server vs Client Components
Default to Server Components, use Client Components only when needed:
// ✅ Server Component (default) - Can fetch data directly export default async function JobPage({ params }) { const job = await fetch(`${process.env.API_URL}/jobs/${params.id}`); return <JobDetails job={job} />; } // ✅ Client Component - Has interactivity 'use client'; export function JobApplicationForm({ jobId }) { const [formData, setFormData] = useState({}); return <form>...</form>; }
Use Client Components when you need:
- useState, useEffect, or other hooks
- Event handlers (onClick, onChange, etc.)
- Browser APIs (localStorage, window, etc.)
- Real-time updates or subscriptions
See references/server-vs-client-components.md.
4. Loading States
Each section should have independent loading states:
<div> {loading ? ( <div className="skeleton h-32 w-full"></div> ) : ( <JobHeader job={job} /> )} {applicationsLoading ? ( <div className="loading loading-spinner"></div> ) : ( <ApplicationsList applications={applications} /> )} </div>
See examples/loading-states.tsx for patterns.
5. Error Handling
Show errors at section level, not page level:
{error && ( <div className="alert alert-error"> <i className="fa-duotone fa-regular fa-circle-exclamation"></i> <span>{error}</span> </div> )} {jobError && <ErrorAlert message={jobError} />} {applicationsError && <ErrorAlert message={applicationsError} />}
See examples/error-handling.tsx for patterns.
Routing Patterns
Protected Routes
All authenticated routes go in
app/portal/ route group:
// apps/portal/src/app/portal/jobs/[id]/page.tsx export default function JobDetailPage({ params }) { // Automatically protected by Clerk middleware }
Never create duplicate route groups - always use
portal for protected pages.
Dynamic Routes
// [id]/page.tsx - Single dynamic segment export default function DetailPage({ params }: { params: { id: string } }) { const { id } = params; } // [...slug]/page.tsx - Catch-all route export default function CatchAllPage({ params }: { params: { slug: string[] } }) { const path = params.slug.join('/'); }
See references/routing-patterns.md for more.
Form Patterns
Use DaisyUI Fieldset Pattern
<fieldset className="fieldset"> <legend className="fieldset-legend">Job Title *</legend> <input type="text" className="input w-full" value={title} onChange={(e) => setTitle(e.target.value)} required /> </fieldset>
See
docs/guidance/form-controls.md for complete form control standards.
Form Submission
async function handleSubmit(e: FormEvent) { e.preventDefault(); setSubmitting(true); setError(null); try { const { data } = await client.post('/jobs', formData); router.push(`/portal/jobs/${data.id}`); } catch (err) { setError(err.message || 'Failed to create job'); } finally { setSubmitting(false); } }
See examples/form-submission.tsx for complete pattern.
List/Table Patterns
Server-Side Filtering Required
// ✅ CORRECT - Server-side filtering with pagination const [params, setParams] = useState({ page: 1, limit: 25, search: '', status: 'active' }); useEffect(() => { async function loadJobs() { const query = new URLSearchParams({ page: params.page.toString(), limit: params.limit.toString(), search: params.search, status: params.status }); const { data, pagination } = await client.get(`/jobs?${query}`); setJobs(data); setPagination(pagination); } loadJobs(); }, [params]); // ❌ WRONG - Client-side filtering doesn't scale const filteredJobs = allJobs.filter(job => job.status === 'active');
See examples/list-with-filtering.tsx.
Search Debouncing
const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { const timer = setTimeout(() => { setParams({ ...params, search: searchTerm, page: 1 }); }, 300); // 300ms debounce return () => clearTimeout(timer); }, [searchTerm]);
See examples/debounced-search.tsx.
Modal/Drawer Patterns
Lazy Load Modal Data
Only fetch data when modal opens:
const [isOpen, setIsOpen] = useState(false); const [modalData, setModalData] = useState(null); const [loading, setLoading] = useState(false); async function openModal() { setIsOpen(true); setLoading(true); const { data } = await client.get(`/applications/${id}`); setModalData(data); setLoading(false); } // ❌ WRONG - Don't load modal data upfront useEffect(() => { // Loads data user might never see loadAllModalData(); }, []);
Performance Optimization
Image Optimization
import Image from 'next/image'; <Image src="/logo.png" alt="Splits Network" width={200} height={50} priority // Above the fold images />
Dynamic Imports
// Lazy load heavy components const HeavyChart = dynamic(() => import('./HeavyChart'), { loading: () => <div className="loading loading-spinner"></div>, ssr: false // Don't render on server });
See references/performance-checklist.md.
Common Anti-Patterns to Avoid
❌ Monolithic Data Loading
// WRONG - Loads everything sequentially const job = await fetchJob(); const applications = await fetchApplications(); const candidates = await fetchCandidates(); // Page blank for 5+ seconds
❌ Over-Fetching
// WRONG - Loading data "just in case" useEffect(() => { loadAllJobs(); loadAllCandidates(); loadAllApplications(); // Most of this data is never used }, []);
❌ Client-Side Filtering at Scale
// WRONG - Breaks with large datasets const filtered = allJobs.filter(j => j.status === 'active');
❌ Blocking Entire Page for Secondary Data
// WRONG - Waiting for everything before showing anything if (!job || !applications || !candidates) { return <Loading />; }
References
- Progressive Loading Example
- API Client Usage
- Form Patterns
- Server vs Client Components
- Performance Checklist
- Routing Patterns
Related Skills
- Backend API patternsapi-specifications
- Error handling strategieserror-handling
- Advanced performance patternsperformance-optimization