Apollo Client (React) - Web SDK
On this page
GraphQL is deprecated. Learn More.
Overview
You can use Apollo Client to connect to your Realm app's exposed GraphQL API from a React application. Apollo Client runs queries and mutations, maintains a client-side data cache, and integrates into your app with idiomatic React components and hooks.
Note
See a Working Demo Application
Check out the Realm GraphQL - Apollo (React) repository on GitHub to see
a fully set-up React & Apollo application that's ready to connect to your own
Atlas App Services backend. It uses the sample_mflix.movies
collection that's
included in the MongoDB Atlas sample data sets.
If you don't want to clone the repository, the demo application is also available in-browser in the Realm GraphQL CodeSandbox.
Set Up Apollo Client
Install Dependencies
As in any Realm project, you'll need to install the Realm Web SDK to authenticate users and requests.
npm install realm-web
Apollo bundles the core components that you need to create a client in a package called @apollo/client. It also requires the graphql package to parse GraphQL queries.
npm install @apollo/client graphql
Create an Apollo GraphQL Client
Create a new ApolloClient object that points to your Realm app's GraphQL API endpoint. You generate the endpoint URL based on your Realm App ID or find it on the GraphQL page of the Realm UI.
// Add your App ID const graphqlUri = `https://services.cloud.mongodb.com/api/client/v2.0/app/${APP_ID}/graphql`; // Local apps should use a local URI! // const graphqlUri = `https://us-east-1.aws.services.cloud.mongodb.com/api/client/v2.0/app/${APP_ID}/graphql` // const graphqlUri = `https://eu-west-1.aws.services.cloud.mongodb.com/api/client/v2.0/app/${APP_ID}/graphql` // const graphqlUri = `https://ap-southeast-1.aws.services.cloud.mongodb.com/api/client/v2.0/app/${APP_ID}/graphql` const client = new ApolloClient({ link: new HttpLink({ uri: graphqlUri, }), cache: new InMemoryCache(), });
Set Up User Authentication
The ApolloClient
is configured to send requests to your app. However, all
Realm GraphQL requests must include a valid user access token to authenticate
requests, so right now any operations sent from Apollo will fail. Access tokens expire after 30 minutes
and need to be refreshed.
To authenticate requests, you need to add an Authorization header with a valid Realm user access token to each GraphQL request.
You can authenticate a user and get their access token with the Realm
Web SDK. The Apollo HttpLink
object allows you to add custom
headers to every request by defining a custom fetch
function.
// Connect to your MongoDB Realm app const app = new Realm.App(APP_ID); // Gets a valid Realm user access token to authenticate requests async function getValidAccessToken() { // Guarantee that there's a logged in user with a valid access token if (!app.currentUser) { // If no user is logged in, log in an anonymous user. The logged in user will have a valid // access token. await app.logIn(Realm.Credentials.anonymous()); } else { // An already logged in user's access token might be stale. Tokens must be refreshed after // 30 minutes. To guarantee that the token is valid, we refresh the user's access token. await app.currentUser.refreshAccessToken(); } return app.currentUser.accessToken; } // Configure the ApolloClient to connect to your app's GraphQL endpoint const client = new ApolloClient({ link: new HttpLink({ uri: `https://services.cloud.mongodb.com/api/client/v2.0/app/${APP_ID}/graphql`, // We define a custom fetch handler for the Apollo client that lets us authenticate GraphQL requests. // The function intercepts every Apollo HTTP request and adds an Authorization header with a valid // access token before sending the request. fetch: async (uri, options) => { const accessToken = await getValidAccessToken(); options.headers.Authorization = `Bearer ${accessToken}`; return fetch(uri, options); }, }), cache: new InMemoryCache(), });
Add the Apollo Client to Your App
The Apollo client
object is now configured to send authenticated GraphQL
requests to your App Services backend. All that's left to do is make it available
to the rest of your React application.
The @apollo/client
package exports an ApolloProvider
component that
makes the client
available to any Apollo hooks that you call from child
components. Wrap your app in an ApolloProvider
and pass the client
object to the provider.
// ... code to create the GraphQL client const AppWithApollo = () => ( <ApolloProvider client={client}> <App /> </ApolloProvider> );
Run Queries and Mutations
The @apollo/client
package includes a set of declarative React hooks that
connect your components to the GraphQL API and handle query and mutation
execution.
To define queries and mutations that you can pass to the hooks, install graphql-tag:
npm install graphql-tag
Import the relevant hooks and the GraphQL query constructor at the top of the file where you're using them.
// import whichever Apollo hooks you're using import { useQuery, useMutation } from "@apollo/client"; import gql from "graphql-tag";
Note
Apollo Hooks Must Have An ApolloProvider
Components that call the query and mutation hooks must be descendants of the
ApolloProvider that
you configured for your App Services backend. The hooks call the query and mutation methods on the
provided client
object.
Run a Query
Apollo Client includes two hooks for executing queries. The hooks accept identical parameters but differ in when they execute the query:
useQuery() runs automatically when its component mounts. It also returns a callback that re-runs the query whenever you call it.
useLazyQuery() returns a callback function that executes the query whenever you call it. It does not run the query on component mount.
Both hooks accept a query definition and additional options, including
variables
that Apollo passes to the query. They also both return information
about the query's current execution status and data returned from the most
recent execution.
const ALL_MOVIES = gql` query AllMovies { movies { _id title year runtime } } `; // Must be rendered inside of an ApolloProvider function Movies() { const { loading, error, data } = useQuery(ALL_MOVIES); if (loading) { return <div>loading</div>; } if (error) { return <div>encountered an error: {error}</div>; } return <MovieList movies={data.movies} />; }
Run a Mutation
The useMutation() hook
accepts a mutation definition and an optional configuration object. The most
common option you'll need to pass is a variables
object that maps to
GraphQL variables in the mutation
definition.
The hook returns several objects in an array:
a callback function that executes the mutation
an object that includes information on the mutation's execution status and data returned from the most recent execution.
const UPDATE_MOVIE_TITLE = gql` mutation UpdateMovieTitle($oldTitle: String!, $newTitle: String!) { updateOneMovie(query: { title: $oldTitle }, set: { title: $newTitle }) { title year } } `; // Must be rendered inside of an ApolloProvider function MovieList({ movies }) { const [updateMovieTitle] = useMutation(UPDATE_MOVIE_TITLE); return ( <ul> {movies.map((movie) => ( <li key={movie._id}> <div>{movie.title}</div> <button onClick={() => { updateMovieTitle({ variables: { oldTitle: movie.title, newTitle: "Some New Title", }, }); }} > Update Title </button> </li> ))} </ul> ); }
Paginate Data
You can paginate data in your queries with the types provided by your API's
generated GraphQL schema. You can paginate data with the Apollo GraphQL client
using the useQuery()
and useLazyQueryHook()
hooks.
The Atlas GraphQL API does not have an offset
operator, like the
GraphQL documentation recommends for pagination.
The following example uses the useQuery()
hook and a GraphQL query that
can query data in ascending and descending order, depending on the variables
you pass to it.
const PAGINATE_MOVIES = gql` query PaginateMovies( $prevTitle: String $nextTitle: String $limit: Int! $sortDirection: MovieSortByInput! ) { movies( # Can add other query filters here if you'd like query: { title_gt: $prevTitle, title_lt: $nextTitle } limit: $limit sortBy: $sortDirection ) { _id title year } } `; const resultsPerPage = 5; function PaginateMovies() { const [variables, setVariables] = useState({ prevTitle: undefined, nextTitle: undefined, limit: resultsPerPage, sortDirection: "TITLE_ASC", }); const [firstTitle, setFirstTitle] = useState(); const { data, error, loading } = useQuery(PAGINATE_MOVIES, { variables, }); const [pagePreviousDisabled, setPagePreviousDisabled] = useState(true); const [pageNextDisabled, setPageNextDisabled] = useState(false); useEffect(() => { if (data?.movies?.length && firstTitle === undefined) { setFirstTitle(data.movies[0].title); } setPagePreviousDisabled(false); if (data?.movies?.length < resultsPerPage) { setPageNextDisabled(true); setPagePreviousDisabled(false); } if ( variables.prevTitle === undefined || data?.movies[0]?.title === firstTitle ) { setPagePreviousDisabled(true); setPageNextDisabled(false); } }, [data, data?.movies?.length, firstTitle, variables.prevTitle]); if (loading) { return <div>loading</div>; } if (error) { return <div>encountered an error: {error.message}</div>; } function goToNextPage() { setVariables({ nextTitle: undefined, prevTitle: data.movies[data.movies.length - 1].title, limit: resultsPerPage, sortDirection: "TITLE_ASC", }); } function goToPrevPage() { setVariables({ nextTitle: data.movies[0].title, prevTitle: undefined, limit: resultsPerPage, sortDirection: "TITLE_DESC", }); } const sorted = data.movies.sort((a, b) => { const titleA = a.title.toUpperCase(); // ignore upper and lowercase const titleB = b.title.toUpperCase(); // ignore upper and lowercase if (titleA < titleB) { return -1; // titleA comes first } if (titleA > titleB) { return 1; // titleB comes first } }); return ( <div> <h1>Movies</h1> {data?.movies?.length ? ( sorted.map((movie) => ( <div key={movie._id}> <h3>{movie.title}</h3> <p>Year Published: {" " + movie.year}</p> <br /> </div> )) ) : ( <p>No movies in system</p> )} <div> <button disabled={pagePreviousDisabled} onClick={goToPrevPage}> ← Previous Page </button> <button disabled={pageNextDisabled} onClick={goToNextPage}> Next Page → </button> </div> </div> ); }
Refresh Access Tokens
When using Realm GraphQL and an Apollo Client, access tokens expire 30 minutes after they're granted.
You can refresh user access tokens with the Realm Web SDK's refreshAccessToken()
method.
// Connect to your MongoDB Realm app const app = new Realm.App(APP_ID); // Gets a valid Realm user access token to authenticate requests async function getValidAccessToken() { // Guarantee that there's a logged in user with a valid access token if (!app.currentUser) { // If no user is logged in, log in an anonymous user. The logged in user will have a valid // access token. await app.logIn(Realm.Credentials.anonymous()); } else { // An already logged in user's access token might be stale. Tokens must be refreshed after // 30 minutes. To guarantee that the token is valid, we refresh the user's access token. await app.currentUser.refreshAccessToken(); } return app.currentUser.accessToken; } // Configure the ApolloClient to connect to your app's GraphQL endpoint const client = new ApolloClient({ link: new HttpLink({ uri: `https://services.cloud.mongodb.com/api/client/v2.0/app/${APP_ID}/graphql`, // We define a custom fetch handler for the Apollo client that lets us authenticate GraphQL requests. // The function intercepts every Apollo HTTP request and adds an Authorization header with a valid // access token before sending the request. fetch: async (uri, options) => { const accessToken = await getValidAccessToken(); options.headers.Authorization = `Bearer ${accessToken}`; return fetch(uri, options); }, }), cache: new InMemoryCache(), });