React Context API
Overview
React Context is used for global state that needs to be shared across multiple components without prop drilling. The application uses Context for:
- Teams and Permissions - User teams, campaigns, and permissions
- Petition State - Current petition data and metadata
- Signature Workflow - Step navigation and signature collection
- Signature Results - Voter search results and form data
- Campaign Visibility - Field visibility rules based on campaign settings
Context Providers Structure
context/
├── TeamsContext.tsx # Teams, campaigns, permissions
├── petitionContext.tsx # Petition data and metadata
├── SignatureStepContext.tsx # Signature workflow steps
├── signatureResultsContext/ # Signature search and form state
├── CampaignVisibilityContext.tsx # Field visibility rules
└── contextTypes.ts # Shared type definitions
1. TeamsContext
Purpose: Manages user teams, campaigns, and permissions data globally.
Location: src/context/TeamsContext.tsx
State Managed:
- User teams list
- Campaigns list
- User permissions (from middleware)
- Active team selection
Implementation:
"use client";
import { createContext, useContext } from "react";
const TeamsContext = createContext<
| (SessionContext & {
teams: UserTeamsRecordType["teams"];
campaigns: CampaignType[];
})
| undefined
>(undefined);
export const TeamsProvider = ({
children,
teams,
campaigns,
permissions_Data,
}: {
children: React.ReactNode;
teams: UserTeamsRecordType["teams"];
campaigns: CampaignType[];
permissions_Data: SessionContext;
}) => {
return (
<TeamsContext.Provider value={{ ...permissions_Data, teams, campaigns }}>
{children}
</TeamsContext.Provider>
);
};
export const useTeams = () => {
const context = useContext(TeamsContext);
if (!context) {
throw new Error("useTeams must be used within a TeamsProvider");
}
return context;
};
Usage:
'use client';
import { useTeams } from '@/context/TeamsContext';
export function MyComponent() {
const { teams, campaigns, permissions_Data } = useTeams();
return (
<div>
<TeamSwitcher teams={teams} />
<CampaignList campaigns={campaigns} />
</div>
);
}
Provider Setup (in layout):
// app/[primaryTeam]/layout.tsx
export default async function RootLayout({ children, params }) {
const teams = await getUserTeams();
const campaigns = await getCampaigns(params.primaryTeam);
const permissionData = await acessCheck({
teamId: params.primaryTeam,
});
return (
<TeamsProvider
teams={teams?.data?.teams}
campaigns={campaigns.data}
permissions_Data={permissionData?.data}
>
{children}
</TeamsProvider>
);
}
Key Features:
- ✅ Server-fetched initial data - Data fetched in Server Component layout
- ✅ Read-only context - No state updates, only data access
- ✅ Type-safe - Full TypeScript support
- ✅ Error boundaries - Throws error if used outside provider
2. PetitionContext
Purpose: Manages petition data, circulator information, and general metadata for signature collection.
Location: src/context/petitionContext.tsx
State Managed:
signatureState- Current petition data (PetitionPageResponse)recentCirculators- Recently used circulatorsgeneralData- Previous values, defaults, and metadata
Implementation:
"use client";
import { createContext, useState, ReactNode } from "react";
type PetitionContextType = {
signatureState: PetitionPageResponse;
setSignatureState: React.Dispatch<React.SetStateAction<PetitionPageResponse>>;
recentCirculators: CirculatorCampaignRelation[];
setRecentCirculators: React.Dispatch<React.SetStateAction<CirculatorCampaignRelation[]>>;
generalData: GeneralDataType;
setGeneralData: React.Dispatch<React.SetStateAction<GeneralDataType>>;
};
export const PetitionContext = createContext<PetitionContextType>({
// Default values...
});
export const PetitionContextProvider = ({
initialValues,
children,
initialCirculators = [],
initialGeneralData = defaultGeneralData,
}: SignatureContextProviderProps) => {
const [signatureState, setSignatureState] = useState(initialValues);
const [recentCirculators, setRecentCirculators] = useState(initialCirculators);
const [generalData, setGeneralData] = useState(initialGeneralData);
return (
<PetitionContext.Provider
value={{
signatureState,
setSignatureState,
recentCirculators,
setRecentCirculators,
generalData,
setGeneralData,
}}
>
{children}
</PetitionContext.Provider>
);
};
export const usePetitionContext = () => {
const context = React.useContext(PetitionContext);
if (!context) {
throw new Error("usePetitionContext must be used within a PetitionContextProvider");
}
return context;
};
Usage:
'use client';
import { usePetitionContext } from '@/context/petitionContext';
export function SignatureForm() {
const {
signatureState,
setSignatureState,
generalData,
recentCirculators
} = usePetitionContext();
const updateCounty = (county: string) => {
setSignatureState(prev => ({
...prev,
county: county
}));
};
return (
<div>
<CountySelect
defaultValue={generalData.defaultCounty}
onChange={updateCounty}
/>
</div>
);
}
Key Features:
- ✅ Mutable state - Can update petition data
- ✅ Initial values from server - Receives data from Server Component
- ✅ Nested state updates - Handles complex nested objects
- ✅ Default values - Provides sensible defaults
3. SignatureStepContext
Purpose: Manages the current step in the multi-step signature collection workflow.
Location: src/context/SignatureStepContext.tsx
State Managed:
currentStep- Current step number (string)totalSteps- Total number of stepssetStep- Function to navigate to a stepsetTotalSteps- Function to set total steps
Implementation:
"use client";
import { createContext, useContext, useState, useCallback, useEffect } from "react";
import { useSearchParams } from "next/navigation";
interface SignatureStepContextType {
currentStep: string;
setStep: (step: string) => string;
setTotalSteps: (totalSteps: number) => void;
totalSteps: number;
}
export const SignatureStepProvider = ({
children,
defaultStep = "1",
}: {
children: ReactNode;
defaultStep?: string;
}) => {
const [currentStep, setCurrentStepInternal] = useState<string>(defaultStep);
const [totalSteps, setTotalSteps] = useState<number>(0);
const searchParams = useSearchParams();
// Sync with URL search params
useEffect(() => {
const step = searchParams.get("step") || defaultStep;
setCurrentStepInternal(step);
}, [searchParams]);
const setStep = useCallback((step: string) => {
setCurrentStepInternal(step);
return step;
}, []);
return (
<SignatureStepContext.Provider value={{ currentStep, setStep, setTotalSteps, totalSteps }}>
{children}
</SignatureStepContext.Provider>
);
};
Key Features:
- ✅ URL synchronization - Syncs with
?step=URL parameter - ✅ Memoized callbacks - Uses
useCallbackfor performance - ✅ Default step - Configurable default step
4. SignatureResultContext
Purpose: Manages complex signature collection state including voter search results, form data arrays, and validation status.
Location: src/context/signatureResultsContext/signatureResultContext.tsx
State Managed:
activeStep- Current view ("form" | "validate")searchResults- Voter search results arrayformDataArray- Array of form entries for signature collectionactivePage- Current page number in petition formatselectedCounty- Selected county for searchcity- City filter for searchtotalPages- Total pages in formatrequiredSignatures- Required signatures for current pagestatus- Search status ("idle" | "loading" | "error")preCreatedPetition- Pre-created petition for faster navigation
Implementation:
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { useDebounce } from "@/hooks/use-debounce";
export const SignatureResultContextProvider = ({
children,
jurisdictionName,
jurisdictionType,
}: {
children: React.ReactNode;
jurisdictionType: string | null;
jurisdictionName: string | null;
}) => {
const [activeStep, setActiveStep] = useState<"form" | "validate">("form");
const [searchResults, setSearchResults] = useState<VoterSearchResultTypeExtended[]>([]);
const [formDataArray, setFormDataArray] = useState<ESFormEntry[]>([]);
const [activePage, setActivePage] = useState(1);
const [selectedCounty, setSelectedCounty] = useState<string | undefined>("");
const [city, setCity] = useState<string>();
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
const debouncedFormData = useDebounce(formDataArray, 500);
// Auto-search when form data changes (debounced)
useEffect(() => {
// Process filled entries and trigger searches
const { filledEntries } = processFilledEntries({
formDataArray: debouncedFormData,
searchResults,
setSearchResults,
});
// Trigger searches for filled entries
filledEntries.forEach((entry) => {
searchMultipleAndReturn([entry], params?.slug).then((newResult) => {
setSearchResults((latestResults) => {
return updateSearchResults(newResult, entry, latestResults);
});
});
});
}, [debouncedFormData]);
return (
<SignatureResultContext.Provider value={{
activeStep,
searchResults,
formDataArray,
setActiveStep,
setSearchResults,
setFormDataArray,
activePage,
setActivePage,
selectedCounty,
setSelectedCounty,
city,
setCity,
totalPages,
requiredSignatures,
status,
preCreatedPetition,
}}>
{children}
</SignatureResultContext.Provider>
);
};
Key Features:
- ✅ Debounced search - Auto-searches when form data changes (500ms delay)
- ✅ Complex state management - Manages arrays, nested objects, and derived state
- ✅ Status tracking - Tracks loading/error states
- ✅ Optimistic updates - Pre-creates petitions for faster navigation
- ✅ Integration with hooks - Uses custom hooks for search logic
5. CampaignVisibilityContext
Purpose: Provides field visibility rules based on campaign configuration.
Location: src/context/CampaignVisibilityContext.tsx
State Managed:
campaign- Campaign data with credentialsshouldRenderField- Function to check if a field should be visible
Implementation:
"use client";
import { createContext, useContext } from "react";
interface CampaignVisibilityContextValue {
campaign: CampaignTypeES | null;
shouldRenderField: (fieldKey: string) => boolean;
}
export const CampaignVisibilityProvider = ({
children,
campaign = null
}) => {
const shouldRenderField = (fieldKey: string): boolean => {
if (!campaign) return false;
// Check circulation fields
if (campaign.circulationFields && fieldKey in campaign.circulationFields) {
return campaign.circulationFields[fieldKey] === true;
}
// Check turnInDate field
if (fieldKey === "turnInDate") {
return campaign.turnInDateRequired === true;
}
// Check county field
if (fieldKey === "county") {
return campaign.addJurisdiction === "county";
}
return true;
};
return (
<CampaignVisibilityContext.Provider value={{ campaign, shouldRenderField }}>
{children}
</CampaignVisibilityContext.Provider>
);
};
Usage:
'use client';
import { useCampaignVisibility } from '@/context/CampaignVisibilityContext';
export function PetitionForm() {
const { shouldRenderField } = useCampaignVisibility();
return (
<form>
{shouldRenderField('county') && (
<CountyField />
)}
{shouldRenderField('turnInDate') && (
<TurnInDateField />
)}
</form>
);
}
Key Features:
- ✅ Conditional rendering - Determines field visibility
- ✅ Centralized logic - Single source of truth for visibility rules
- ✅ Type-safe - Full TypeScript support