Skip to main content

Frontend Context & Provider Patterns

Read rules/frontend.md first for core architecture, routing, and project conventions.

The src/context/team-provider.tsx + src/features/teams/detail/index.tsx pair is the primary reference implementation.


Core Pattern: Route-Level Fetching with Context Sharing

Rule: The route component fetches data, handles errors/redirects, then provides clean data to a context. Providers never fetch internally.

// ❌ DON'T: Provider fetches internally — errors are hard to handle
<TeamProvider teamSlug={teamSlug}>
{/* Provider must handle errors itself, or they bubble up */}
</TeamProvider>

// ✅ DO: Route fetches, handles errors, then provides guaranteed data
const { data: team, isError, isLoading } = useTeam(teamSlug)
if (isLoading) return <LoadingSpinner />
if (isError || !team) { navigate(...); return null }
<TeamProvider team={team}>
{/* Children receive guaranteed valid data */}
</TeamProvider>

Why: Providers shouldn't handle routing/navigation. Route-level handling enables redirects and clean loading states.


Context Guarantees Valid Data

Context consumers can assume the data exists and is valid — no null checks needed in child components.

// ✅ Context consumer — no null check needed
const { team } = useTeamContext()
return <h1>{team.name}</h1>

// ❌ DON'T spread null-checking through every consumer
const { team } = useTeamContext()
if (!team) return <div>Team not found</div>

Hook Design: Single Responsibility with DRY

Expose separate public hooks for different access patterns; share internal logic.

// Public APIs — clear intent, one responsibility each
export function useMembers(teamId: string) {
const { data, isLoading, isError } = useMembersQuery(teamId)
return { data: data || [], isLoading, isError }
}

export function useMembersBySlug(teamSlug: string) {
const { entityId: teamId, isLoading: isSlugLoading, isError: isSlugError } = useSlugToEntityId(teamSlug)
const { data, isLoading: isMembersLoading, isError: isMembersError } = useMembersQuery(teamId || '')
return {
data: data || [],
isLoading: isSlugLoading || isMembersLoading,
isError: isSlugError || isMembersError
}
}

// Internal helper — complex logic in one place, not exported
function useMembersQuery(teamId: string) {
// All GraphQL logic, data transformation, etc.
}

Route-Level Error Handling

useEffect(() => {
if (!isLoading && (isError || !team)) {
navigate({
to: '/_authenticated/o/$orgSlug',
params: { orgSlug },
replace: true // Clean browser history
})
}
}, [isLoading, isError, team, navigate, orgSlug])

Performance

  • Single fetch per route: Route fetches once, all children share via context — no duplicate requests
  • Context memoization: Use useMemo for the context value object to avoid recreating it on every render
  • TanStack Query caching: Data is cached; context just distributes it

Common Anti-Patterns

// ❌ Provider fetches internally — hard to handle errors gracefully
function TeamProvider({ teamSlug }) {
const { data: team } = useTeam(teamSlug)
return <Context.Provider value={team}>...</Context.Provider>
}

// ❌ Null checks scattered through every child component
const { team } = useTeamContext()
if (!team) return <div>Team not found</div>

// ❌ Provider handles navigation (mixed responsibilities)
function TeamProvider({ teamSlug }) {
const { data: team, error } = useTeam(teamSlug)
if (error) navigate('/error') // Navigation belongs in the route
}

When to Use This Pattern

Use when:

  • Multiple sibling/child components need the same fetched data
  • Route-based error handling and redirects are needed
  • Components shouldn't need to handle missing data cases
  • Type safety and clean component APIs are priorities

Don't use when:

  • Only one component needs the data — just fetch directly in that component
  • Simple prop drilling is sufficient (1–2 levels)
  • Real-time data updates are critical (context re-renders all consumers)

Reference Implementation

  • src/context/team-provider.tsx — context provider
  • src/features/teams/detail/index.tsx — route-level fetching + provider usage
  • src/features/members/hooks/use-members.ts — DRY hook design pattern
  • src/features/members/index.tsx — context consumer example
  • src/context/application-provider.tsx — application context (same pattern)