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:

  • teams — the user's teams
  • campaigns — campaigns the user can reach
  • permissionKeys — flat array of permission key slugs from the snapshot (drives sidebar visibility)
  • superAdminTeamId — id of the super-admin team (hydrated from SUPER_ADMIN_TEAM_ID)
  • userTeamRole and other fields carried by SessionContext

Implementation:

"use client";
import { createContext, useContext } from "react";

const TeamsContext = createContext<
| (SessionContext & {
teams: UserTeamsRecordType["teams"];
campaigns: CampaignType[];
superAdminTeamId: string;
})
| undefined
>(undefined);

export const TeamsProvider = ({
children,
teams,
campaigns,
permissions_Data,
superAdminTeamId,
}: {
children: React.ReactNode;
teams: UserTeamsRecordType["teams"];
campaigns: CampaignType[];
permissions_Data: SessionContext;
superAdminTeamId: string;
}) => {
return (
<TeamsContext.Provider
value={{ ...permissions_Data, teams, campaigns, superAdminTeamId }}
>
{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';
import { useParams } from 'next/navigation';

export function MyComponent() {
const params = useParams<{ primaryTeam: string }>();
const { teams, campaigns, permissionKeys, superAdminTeamId } = useTeams();

const canSeeMembers =
permissionKeys?.includes("team-member-page") ||
params.primaryTeam === superAdminTeamId;

return (
<div>
<TeamSwitcher teams={teams} />
{canSeeMembers && <MembersLink />}
</div>
);
}

Provider Setup (in the team server layout):

// app/[primaryTeam]/(team-layout)/layout.tsx
import { redirect } from "next/navigation";
import { getRoutePermissions } from "@/services/accessCheck/getRoutePermissions";
import { getUserTeams } from "@/services/teams/getUserTeams";
import { getCampaignsForUser } from "@/services/campaign/getCampaignsForUser";
import { SUPER_ADMIN_TEAM_ID } from "@/lib/constants/superAdmin";

export default async function TeamLayout({ children, params }) {
const permissions = await getRoutePermissions({ teamId: params.primaryTeam });
if (!permissions.teamAccess) redirect("/");

const [teams, campaigns] = await Promise.all([
getUserTeams(),
getCampaignsForUser(params.primaryTeam),
]);

return (
<TeamsProvider
teams={teams?.data?.teams}
campaigns={campaigns.data}
permissions_Data={{ permissionKeys: permissions.permissionKeys ?? [] }}
superAdminTeamId={SUPER_ADMIN_TEAM_ID}
>
{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 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