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
useMemofor 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 providersrc/features/teams/detail/index.tsx— route-level fetching + provider usagesrc/features/members/hooks/use-members.ts— DRY hook design patternsrc/features/members/index.tsx— context consumer examplesrc/context/application-provider.tsx— application context (same pattern)