Client Components
Overview
Client Components are React components that run in the browser. They're marked with "use client" and can use React hooks, event handlers, and browser APIs.
When to Use Client Components
- ✅ Interactive UI (buttons, forms, modals)
- ✅ Browser APIs (localStorage, window)
- ✅ React hooks (useState, useEffect)
- ✅ Event handlers (onClick, onChange)
- ✅ Real-time updates
- ✅ Client-side data fetching
Implementation Pattern
'use client';
import { useState, useEffect } from 'react';
import { createClient } from '@/lib/supabase/client';
export function InteractiveComponent() {
const [data, setData] = useState([]);
useEffect(() => {
// Client-side data fetching
fetchData();
}, []);
return <div>{/* Interactive UI */}</div>;
}
Client-Side Data Fetching Patterns
1. Table Data Fetching Hook
The application uses a custom hook useFetchTableData for client-side table data:
// components/tables/common/useFetchTableData.tsx
'use client';
import { createClient } from "@/lib/supabase/client";
export function useFetchTableData({ config, campaignId, tableState }) {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const fetchData = useCallback(async () => {
setIsLoading(true);
const supabase = createClient(); // Browser client
let query = supabase
.from(config.tableName)
.select(config.selectQuery, { count: "exact" });
// Apply filters, sorting, pagination
query = applyFilters(query, tableState);
const { data, error, count } = await query;
setData(data);
setIsLoading(false);
}, [tableState]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, isLoading, refetch: fetchData };
}
Features:
- ✅ Client-side caching with
useRef - ✅ Automatic refetch on filter/sort/pagination changes
- ✅ Loading and error states
- ✅ Pagination support
2. Voter Search Hook
// hooks/add-signatures/useESValidation.tsx
'use client';
import { useState } from 'react';
export function useESValidation() {
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const searchVoter = async (searchParams) => {
setIsSearching(true);
try {
const response = await fetch('/api/essearch', {
method: 'POST',
body: JSON.stringify({ params: searchParams }),
});
const data = await response.json();
setResults(data.persons);
} finally {
setIsSearching(false);
}
};
return { results, isSearching, searchVoter };
}
Hybrid Pattern: Server + Client
A common pattern is Server Component fetching initial data, then Client Component handling interactions:
// Server Component (page.tsx)
export default async function Page({ params }) {
const initialData = await getPetitionById(params.petitionId);
return (
<PetitionContextProvider initialValues={initialData.data}>
{/* Client Component receives server-fetched data */}
<SignatureFlow result={initialData} />
</PetitionContextProvider>
);
}
// Client Component (signatureFlow.tsx)
'use client';
export function SignatureFlow({ result }) {
const [step, setStep] = useState(1);
// Uses server-fetched data + client-side state
return <div>{/* Interactive workflow */}</div>;
}