Apollo Integration

Apollo is a great solution for managing an application's data using GraphQL.

There are a few different implementation strategies for integrating Apollo and Curi based on how tightly you want them to be paired.

Note:

This guide only covers integration between Curi and Apollo. If you are not already familiar with how to use Apollo, you will want to learn that first.

Also, this guide will only be referencing Apollo's React implementation, but the principles are the same no matter how you render your application.

Setup

Apollo's React package provides an <ApolloProvider> component for accessing your Apollo client throughout the application. The <Router> (or whatever you name the root Curi component) should be a descendant of the <ApolloProvider> because we don't need to re-render the <ApolloProvider> for every new response.

import { ApolloProvider } from "react-apollo";
import { curiProvider } from "@curi/react-dom";

const Router = curiProvider(router);

ReactDOM.render((
  <ApolloProvider client={client}>
    <Router>
      {() => {...}}
    </Router>
  </ApolloProvider>
), holder);

Loose Pairing

Apollo and Curi don't actually have to know about each other. Curi can create a response without doing any data fetching and let Apollo handle that with its <Query> component.

// routes.js
import Noun from "./pages/Noun";

// nothing Apollo related in here
const routes = prepareRoutes([
  {
    name: 'Noun',
    path: 'noun/:word',
    response: () => {
      return {
        body: Noun
      };
    }
  }
]);

Any location data that a query needs can be taken from the response object. The best way to access this from your components would be to pass the response to the components rendered in the <Router>'s children prop, which is a render-invoked function.

// index.js
ReactDOM.render((
  <ApolloProvider client={client}>
    <Router>
      {({ response }) => {
        const { body:Body } = response;
        return <Body response={response} />;
      }}
    </Router>
  </ApolloProvider>
), holder);

Because we pass the response to the route's body component, we can pass a <Query> the response's location params using props.response.params.

// pages/Nouns.js
import { Query } from "react-apollo";

const GET_NOUN = gql`
  query noun($word: String!) {
    noun(word: $word) {
      word,
      type,
      definition
    }
  }
`;

// use the "word" param from the response props
// to query the correct data
const Noun = ({ response }) => (
  <Query
    query={GET_NOUN}
    variables={{ word: response.params.word }}
  >
    {({ loading, error, data }) => {
      if (loading) {
        return <Loading />;
      }
      // ...

      return (
        <div>
          <h1>{data.noun.word}</h1>
          <p>{data.noun.definition}</p>
        </div>
      )
    }}
  </Query>
);

Tight Pairing

You can use your Apollo client instance to call queries in a route's resolve functions. resolve functions are expected to return a Promise, which is exactly what client.query() returns. Tightly pairing Curi and Apollo is mostly center around using a resolve function to return a client.query() call. This will delay navigation until after a route's GraphQL data has been loaded by Apollo.

The external option can be used when creating the router to make the Apollo client accessible from routes.

import client from "./apollo";
          
const router = curi(history, routes, {
  external: { client }
});
import { EXAMPLE_QUERY } from "./queries";

const routes = prepareRoutes([
  {
    name: "Example",
    path: "example/:id",
    resolve: {
      data({ params }, external) {
        return external.client.query({
          query: EXAMPLE_QUERY,
          variables: { id: params.id }
        });
      }
    }
  }
]);

There are two strategies for doing this.

The first approach is to avoid the <Query> altogether. Instead, you can use a route's response() property to attach the data fetched by Apollo directly to a response through its data property.

While we know at this point that the query has executed, we should also check error in the response() function to ensure that the query was executed successfully.

// routes.js
import GET_VERB from "./queries";

import Verb from "./pages/Verb";

export default [
  {
    name: "Verb",
    path: "verb/:word",
    resolve: {
      verb({ params }, external) {
        return external.client.query({
          query: GET_VERB,
          variables: { word: params.word }
        })
      }
    },
    response({ error, resolved }) {
      if (error) {
        // handle failed queries
      }
      return {
        body: Verb,
        data: resolved.verb.data
      }
    }
  }
];

When rendering, you can access the query data through the response's data property.

// pages/Verb.js
const Verb = ({ response }) => (
  <div>
    <h1>{response.data.verb.word}</h1>
    <p>
      {response.data.verb.definition}
    </p>
  </div>
)

The second approach is to use a resolve function as a way to cache the data, but also use <Query>. With this approach, we do not have to attach the query data to the response; we are just relying on the fact that Apollo will execute and cache the results prior to navigation.

// routes.js
import { GET_VERB } from "./queries";

export default [
  {
    name: "Verb",
    path: "verb/:word",
    resolve: {
      data({ params, external }) {
        // load the data so it is cached by
        // your Apollo client
        return external.client.query({
          query: GET_VERB,
          variables: { word: params.word }
        })
      }
    }
  }
];

The route's component will render a <Query> to also call the query. Because the query has already been executed, Apollo will grab the data from its cache instead of re-sending a request to your server.

// pages/Verb.js
import { GET_VERB } from "../queries";

const Verb = ({ response }) => (
  <Query
    query={GET_VERB}
    variables={{ word: response.params.word }}
  >
    {({ loading, error, data }) => {
      // ...
      return (
        <div>
          <h1>{data.verb.word}</h1>
          <p>
            {data.verb.definition}
          </p>
        </div>
      );
    }}
  </Query>
)

Prefetching

One additional benefit of adding queries to routes using resolve functions is that you can prefetch data for a route.

The @curi/route-prefetch interaction lets you programmatically fetch the data for a route prior to navigating to a location.

// index.js
import prefetch from "@curi/route-prefetch";

const routes = prepareRoutes([
  {
    name: "Example",
    path: "example/:id",
    resolve: {
      examples({ params }, external) {
        return external.client.query({
          query: GET_EXAMPLES,
          variables: { id: params.id }
        })
      }
    }
  }
]);

const router = curi(history, routes, {
  route: [prefetch()]
});

// this will call the GET_EXAMPLES query
// and Apollo will cache the results
router.route.prefetch(
  "Example",
  { params: { id: 2 }}
);