Subchapter 1.9
references/queries.mdMarkdown10 KBView on GitHub
Note: In most applications, there should only be one use of a query hook per page. Use fragment-reading hooks (
useFragment,useSuspenseFragment) with component-colocated fragments and data masking for the rest of the page components.
The useQuery hook is the primary way to fetch data in Apollo Client in non-suspenseful applications. It returns loading and error states that must be handled.
Note: In suspenseful applications, use
useSuspenseQueryoruseBackgroundQueryinstead. See the Suspense Hooks reference for more details.
import { gql } from "@apollo/client";
import { useQuery } from "@apollo/client/react";
const GET_DOGS = gql`
query GetDogs {
dogs {
id
breed
displayImage
}
}
`;
function Dogs() {
const { loading, error, data } = useQuery(GET_DOGS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{data?.dogs.map((dog) => <li key={dog.id}>{dog.breed}</li>)}</ul>;
}const {
data, // Query result data
loading, // True during initial load
error, // ApolloError if request failed
networkStatus, // Detailed network state (1-8)
dataState, // For TypeScript type narrowing (AC 4.x)
refetch, // Function to re-execute query
fetchMore, // Function for pagination
startPolling, // Start polling at interval
stopPolling, // Stop polling
subscribeToMore, // Add subscription to query
updateQuery, // Manually update query result
client, // Apollo Client instance
called, // True if query has been executed
previousData, // Previous data (useful during loading)
} = useQuery(QUERY);const GET_DOG = gql`
query GetDog($breed: String!) {
dog(breed: $breed) {
id
displayImage
}
}
`;
function DogPhoto({ breed }: { breed: string }) {
const { loading, error, data } = useQuery(GET_DOG, {
variables: { breed },
});
if (loading) return null;
if (error) return <p>Error: {error.message}</p>;
return <img src={data.dog.displayImage} alt={breed} />;
}Use TypedDocumentNode instead of generic type parameters for better type safety:
import { gql, TypedDocumentNode } from "@apollo/client";
import { useQuery } from "@apollo/client/react";
interface GetDogData {
dog: {
id: string;
displayImage: string;
};
}
interface GetDogVariables {
breed: string;
}
const GET_DOG: TypedDocumentNode<GetDogData, GetDogVariables> = gql`
query GetDog($breed: String!) {
dog(breed: $breed) {
id
displayImage
}
}
`;
const { data } = useQuery(GET_DOG, {
variables: { breed: "bulldog" },
});
// data?.dog is fully typedfunction DogSelector() {
const [breed, setBreed] = useState("bulldog");
// Query automatically re-runs when breed changes
const { data } = useQuery(GET_DOG, {
variables: { breed },
});
return (
<select value={breed} onChange={(e) => setBreed(e.target.value)}>
<option value="bulldog">Bulldog</option>
<option value="poodle">Poodle</option>
</select>
);
}function UserProfile({ userId }: { userId: string }) {
const { loading, data, previousData } = useQuery(GET_USER, {
variables: { id: userId },
});
// Show previous data while loading new data
const displayData = data ?? previousData;
return (
<div>
{loading && <LoadingSpinner />}
{displayData && <UserCard user={displayData.user} />}
</div>
);
}import { NetworkStatus } from "@apollo/client";
function Dogs() {
const { loading, error, data, networkStatus, refetch } = useQuery(GET_DOGS, {
notifyOnNetworkStatusChange: true,
});
if (networkStatus === NetworkStatus.refetch) {
return <p>Refetching...</p>;
}
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{data.dogs.map((dog) => (
<li key={dog.id}>{dog.breed}</li>
))}
</ul>
</>
);
}Use useLazyQuery when you want to execute a query in response to a user-triggered event (like a button click) rather than on component mount.
Important: useLazyQuery doesn’t guarantee a network request - it only sets variables. If data is already in the cache, this isn’t a “refetch”. Only use useLazyQuery if you consume the second tuple value (loading, data, error states) to synchronize cache data with the component. If you only need the promise, use client.query directly instead.
import { gql } from "@apollo/client";
import { useLazyQuery } from "@apollo/client/react";
const GET_DOG_PHOTO = gql`
query GetDogPhoto($breed: String!) {
dog(breed: $breed) {
id
displayImage
}
}
`;
function DelayedQuery() {
const [getDog, { loading, error, data, called }] =
useLazyQuery(GET_DOG_PHOTO);
if (called && loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data?.dog && <img src={data.dog.displayImage} />}
<button onClick={() => getDog({ variables: { breed: "bulldog" } })}>
Get Bulldog Photo
</button>
</div>
);
}If you only need the promise result and don’t consume the loading/error/data states from the hook, use client.query instead:
import { useApolloClient } from "@apollo/client/react";
function SearchDogs() {
const client = useApolloClient();
const [search, setSearch] = useState("");
const handleSearch = async () => {
try {
const { data } = await client.query({
query: SEARCH_DOGS,
variables: { query: search },
});
console.log("Found dogs:", data.searchDogs);
} catch (error) {
console.error("Search failed:", error);
}
};
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
);
}function LiveFeed() {
const { data, startPolling, stopPolling } = useQuery(GET_FEED, {
pollInterval: 5000, // Poll every 5 seconds
});
// Or control polling dynamically
useEffect(() => {
startPolling(5000);
return () => stopPolling();
}, [startPolling, stopPolling]);
return <Feed items={data?.feed} />;
}function DogList() {
const { data, refetch } = useQuery(GET_DOGS);
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<button onClick={() => refetch({ breed: "poodle" })}>
Refetch Poodles
</button>
<ul>{data?.dogs.map((dog) => <li key={dog.id}>{dog.breed}</li>)}</ul>
</div>
);
}Control how the query interacts with the cache.
| Policy | Description |
|---|---|
cache-first | Return cached data if available, otherwise fetch (default) |
cache-only | Only return cached data, never fetch |
cache-and-network | Return cached data immediately, then fetch and update |
network-only | Always fetch, update cache, ignore cached data |
no-cache | Always fetch, never read or write cache |
standby | Same as cache-first but doesn’t auto-update |
// Real-time data - always fetch
const { data } = useQuery(GET_NOTIFICATIONS, {
fetchPolicy: "network-only",
});
// Static data - prefer cache
const { data } = useQuery(GET_CATEGORIES, {
fetchPolicy: "cache-first",
});
// Show cached data while fetching fresh data
const { data, loading } = useQuery(GET_POSTS, {
fetchPolicy: "cache-and-network",
});
// Fetch once, then use cache
const { data } = useQuery(GET_USER_PROFILE, {
fetchPolicy: "network-only",
nextFetchPolicy: "cache-first",
});// First request: network-only
// Subsequent requests: cache-first
const { data } = useQuery(GET_POSTS, {
fetchPolicy: "network-only",
nextFetchPolicy: "cache-first",
});
// Or use a function for more control
const { data } = useQuery(GET_POSTS, {
fetchPolicy: "network-only",
nextFetchPolicy: (currentFetchPolicy, { reason, observable }) => {
if (reason === "after-fetch") {
return "cache-first";
}
return currentFetchPolicy;
},
});Use skipToken to conditionally skip queries without TypeScript issues:
import { skipToken } from "@apollo/client";
function UserProfile({ userId }: { userId: string | null }) {
const { data } = useQuery(
GET_USER,
!userId ? skipToken : (
{
variables: { id: userId },
}
)
);
return userId ? <Profile user={data?.user} /> : <p>Select a user</p>;
}function UserProfile({ userId }: { userId: string | null }) {
const { data } = useQuery(GET_USER, {
variables: { id: userId! },
skip: !userId, // Don't execute if no userId
});
return userId ? <Profile user={data?.user} /> : <p>Select a user</p>;
}Note: Using
skipTokenis preferred overskipas it avoids TypeScript issues with required variables and the non-null assertion operator.
// Skip during server-side rendering
const { data } = useQuery(GET_USER_LOCATION, {
skip: typeof window === "undefined",
ssr: false,
});