Skip to main content

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:

  1. Teams and Permissions - User teams, campaigns, and permissions
  2. Petition State - Current petition data and metadata
  3. Signature Workflow - Step navigation and signature collection
  4. Signature Results - Voter search results and form data
  5. 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 circulators
  • generalData - 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 steps
  • setStep - Function to navigate to a step
  • setTotalSteps - 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 useCallback for 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 array
  • formDataArray - Array of form entries for signature collection
  • activePage - Current page number in petition format
  • selectedCounty - Selected county for search
  • city - City filter for search
  • totalPages - Total pages in format
  • requiredSignatures - Required signatures for current page
  • status - 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 credentials
  • shouldRenderField - 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