Use your API from a React app with Apollo Client — run queries and mutations with hooks, handle loading and errors, and let the normalized cache keep the UI in sync.
Why: in a real app you do not hand-write fetch calls — a client library manages requests, caching, and state. Apollo Client is the most common. Point it at your endpoint, give it a cache, and wrap your app in the provider so any component can run operations.
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
})
function Root() {
return (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
)
}Why: the useQuery hook runs a query and hands back loading, error, and data — the three states you always render. The query is parsed with the gql tag. Apollo caches the result, so the same query elsewhere is served instantly.
import { gql, useQuery } from '@apollo/client'
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
}
}
`
function Books() {
const { loading, error, data } = useQuery(GET_BOOKS)
if (loading) return <p>Loading...</p>
if (error) return <p>Error loading books</p>
return data.books.map((b) => <p key={b.id}>{b.title}</p>)
}Why: useMutation returns a function you call to run the write, plus its loading/error state. Pass variables when you call it. After a mutation you usually refetch affected queries (or update the cache) so the UI reflects the change.
import { gql, useMutation } from '@apollo/client'
const ADD_BOOK = gql`
mutation AddBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) { id title }
}
`
function AddBook() {
const [addBook, { loading }] = useMutation(ADD_BOOK, {
refetchQueries: ['GetBooks'],
})
return (
<button
disabled={loading}
onClick={() => addBook({ variables: { title: 'Dune', author: 'Herbert' } })}
>
Add
</button>
)
}Note: Apollo's real value is its cache. It normalizes results by type and id (so a Book is stored once, no matter how many queries return it), serves cached data instantly, and updates every component showing that object when it changes. This is why you request id on objects — it is the cache key. urql and Relay offer similar caching with different trade-offs.
query A returns Book:1 ─┐
query B returns Book:1 ─┼─▶ stored ONCE as Book:1 in the cache
mutation updates Book:1 ─┘ every component showing Book:1 re-renders
(this is why objects need an id — it's the cache key)