React Context with Apollo Client
Avoiding props-drilling by making data accessible through a React Hook
React Context provides a way to pass data through the component tree without having to pass props down manually at every level in your application which is also known as props-drilling.
Props-drilling can cause problems when you need to rename a property or update the data type and can cause bloat within your application as each component in the tree needs to be aware of properties they may not be using. Instead our data will be accessible through a React Hook, which can be called anywhere in the nested component tree.
To help show how to avoid props-drilling, we'll first configure two components to use React Context: QueryResultProvider
and useQueryResult
.
The QueryResultProvider
component is used to wrap a component tree, providing it with the context for a specific GraphQL query. The component takes in a query
which is a GraphQL query, and optionally variables
which is passed to the query. It uses the useQuery
hook from the Apollo Client to execute the query and get the query data, loading, and error values. These values are then saved in the context.
import React, { createContext, useContext } from "react";import { ApolloQueryResult, OperationVariables, useQuery } from "@apollo/client";interface QueryResult<TData> {data?: TData;error?: any;loading: boolean;}interface QueryContextValue<TData> {queryData: QueryResult<TData>;refetch: () => Promise<ApolloQueryResult<TData>>;}const QueryResultContext = createContext<QueryContextValue<any>>({queryData: { loading: true },refetch: () => Promise.resolve({} as ApolloQueryResult<any>),});interface QueryResultProviderProps<TVariables extends OperationVariables | undefined = object> {query: any;variables?: TVariables;children: React.ReactNode;}export const QueryResultProvider = ({ query, variables, children }: QueryResultProviderProps) => {const { data, error, loading, refetch } = useQuery(query, { variables });const value = { queryData: { data, error, loading }, refetch };return <QueryResultContext.Provider value={value}>{children}</QueryResultContext.Provider>;};export const useQueryResult = <TData = any,>(): QueryResult<TData> => {const context = useContext(QueryResultContext);if (!context) {throw new Error("useQueryResult must be used within a QueryResultProvider");}return context.queryData;};
In this example, we use the QueryResultProvider
component to wrap the Users
component, providing it with the GET_USERS
query. The Users
component then uses the useQueryResult
hook to access the data, loading state, and error from the query context.
import { QueryProvider, useQueryData } from "./query-context";import { gql } from "@apollo/client";const GET_USERS = gql`query GetUsers {users {idname}}`;interface IUser {id: string;name: string;}const Users = () => {const { data, loading, error } = useQueryResult<IUser>();if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;return (<div><h1>Users</h1><ul>{data.users.map((user) => (<li key={user.id}>{user.name}</li>))}</ul></div>);};const App = () => (<QueryResultProvider query={GET_USERS}><Users /></QueryResultProvider>);