How to use React with GraphQL

GraphQL enables React applications to fetch exactly the data they need in a single request, eliminating over-fetching and under-fetching. As the creator of CoreUI with 12 years of React development experience, I’ve built React GraphQL applications that reduced API payload sizes by 60% while improving response times for millions of users.

The most production-ready approach uses Apollo Client for comprehensive GraphQL state management with caching and real-time updates.

Install Dependencies

npm install @apollo/client graphql

Setup Apollo Client

Create src/apollo/client.js:

import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'

const httpLink = new HttpLink({
  uri: process.env.REACT_APP_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql'
})

const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network'
    }
  }
})

export default client

Wrap App with Provider

import React from 'react'
import ReactDOM from 'react-dom/client'
import { ApolloProvider } from '@apollo/client'
import client from './apollo/client'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
)

Query Data with useQuery

import { gql, useQuery } from '@apollo/client'

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      role
    }
  }
`

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS)

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  )
}

Query with Variables

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`

function UserProfile({ userId }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id: userId }
  })

  if (loading) return <div>Loading user...</div>
  if (error) return <div>Error loading user</div>

  return (
    <div>
      <h1>{data.user.name}</h1>
      <p>{data.user.email}</p>
      <h2>Posts</h2>
      <ul>
        {data.user.posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

Mutations with useMutation

import { gql, useMutation } from '@apollo/client'

const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`

function CreateUserForm() {
  const [name, setName] = React.useState('')
  const [email, setEmail] = React.useState('')

  const [createUser, { loading, error, data }] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }]
  })

  const handleSubmit = async (e) => {
    e.preventDefault()

    try {
      await createUser({
        variables: {
          input: { name, email }
        }
      })

      setName('')
      setEmail('')
      alert('User created successfully!')
    } catch (err) {
      console.error('Error creating user:', err)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
        required
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <div className="error">{error.message}</div>}
    </form>
  )
}

Update Cache After Mutation

const DELETE_USER = gql`
  mutation DeleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
    }
  }
`

function UserItem({ user }) {
  const [deleteUser] = useMutation(DELETE_USER, {
    update(cache, { data: { deleteUser } }) {
      cache.modify({
        fields: {
          users(existingUsers = [], { readField }) {
            return existingUsers.filter(
              userRef => deleteUser.id !== readField('id', userRef)
            )
          }
        }
      })
    }
  })

  const handleDelete = async () => {
    if (window.confirm('Delete this user?')) {
      await deleteUser({ variables: { id: user.id } })
    }
  }

  return (
    <div>
      <span>{user.name}</span>
      <button onClick={handleDelete}>Delete</button>
    </div>
  )
}

Optimistic Updates

const UPDATE_USER = gql`
  mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
    updateUser(id: $id, input: $input) {
      id
      name
      email
    }
  }
`

function EditUser({ user }) {
  const [name, setName] = React.useState(user.name)

  const [updateUser] = useMutation(UPDATE_USER, {
    optimisticResponse: {
      updateUser: {
        __typename: 'User',
        id: user.id,
        name,
        email: user.email
      }
    }
  })

  const handleUpdate = async () => {
    await updateUser({
      variables: {
        id: user.id,
        input: { name }
      }
    })
  }

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button onClick={handleUpdate}>Update</button>
    </div>
  )
}

Pagination

const GET_POSTS = gql`
  query GetPosts($limit: Int!, $offset: Int!) {
    posts(limit: $limit, offset: $offset) {
      id
      title
      author {
        name
      }
    }
  }
`

function PostList() {
  const [page, setPage] = React.useState(0)
  const limit = 10

  const { loading, error, data, fetchMore } = useQuery(GET_POSTS, {
    variables: { limit, offset: page * limit }
  })

  const loadMore = () => {
    fetchMore({
      variables: { offset: (page + 1) * limit },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev
        return {
          posts: [...prev.posts, ...fetchMoreResult.posts]
        }
      }
    })
    setPage(page + 1)
  }

  if (loading && page === 0) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <div>
      <ul>
        {data.posts.map(post => (
          <li key={post.id}>
            {post.title} by {post.author.name}
          </li>
        ))}
      </ul>
      <button onClick={loadMore} disabled={loading}>
        {loading ? 'Loading...' : 'Load More'}
      </button>
    </div>
  )
}

Subscriptions for Real-time Updates

import { gql, useSubscription } from '@apollo/client'

const MESSAGE_SUBSCRIPTION = gql`
  subscription OnMessageAdded {
    messageAdded {
      id
      text
      user {
        name
      }
      createdAt
    }
  }
`

function MessageList() {
  const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION)

  React.useEffect(() => {
    if (data) {
      console.log('New message:', data.messageAdded)
    }
  }, [data])

  return (
    <div>
      {loading ? (
        <div>Connecting...</div>
      ) : (
        <div>
          <div>{data.messageAdded.user.name}:</div>
          <div>{data.messageAdded.text}</div>
        </div>
      )}
    </div>
  )
}

Error Handling

import { ApolloError } from '@apollo/client'

function UserProfile({ userId }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id: userId },
    onError: (error) => {
      console.error('GraphQL error:', error)
    }
  })

  if (loading) return <div>Loading...</div>

  if (error) {
    if (error.networkError) {
      return <div>Network error. Please check your connection.</div>
    }

    if (error.graphQLErrors.length > 0) {
      return (
        <div>
          {error.graphQLErrors.map((err, i) => (
            <div key={i}>{err.message}</div>
          ))}
        </div>
      )
    }

    return <div>An error occurred</div>
  }

  return <div>{data.user.name}</div>
}

Authentication

Setup with auth token:

import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client'

const httpLink = new HttpLink({
  uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
})

const authLink = new ApolloLink((operation, forward) => {
  const token = localStorage.getItem('authToken')

  operation.setContext({
    headers: {
      authorization: token ? `Bearer ${token}` : ''
    }
  })

  return forward(operation)
})

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
})

Custom Hooks

import { gql, useQuery, useMutation } from '@apollo/client'

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`

const UPDATE_USER = gql`
  mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
    updateUser(id: $id, input: $input) {
      id
      name
      email
    }
  }
`

function useUser(userId) {
  const { data, loading, error } = useQuery(GET_USER, {
    variables: { id: userId },
    skip: !userId
  })

  const [updateUser, { loading: updating }] = useMutation(UPDATE_USER)

  const update = async (input) => {
    await updateUser({
      variables: { id: userId, input }
    })
  }

  return {
    user: data?.user,
    loading,
    error,
    update,
    updating
  }
}

// Usage
function UserProfile({ userId }) {
  const { user, loading, update } = useUser(userId)

  if (loading) return <div>Loading...</div>

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => update({ name: 'New Name' })}>
        Update
      </button>
    </div>
  )
}

Best Practice Note

This is the same GraphQL architecture we use in CoreUI’s React admin templates. Apollo Client provides robust caching, optimistic updates, and real-time subscriptions out of the box. Always implement proper error handling for both network and GraphQL errors, and use cache updates or refetchQueries to keep your UI synchronized after mutations.

For production applications, consider using CoreUI’s React Admin Template which includes pre-configured Apollo Client setup with authentication and error handling.

For related data fetching patterns, check out how to fetch data in React and how to use React Query.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team