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.

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 { createRouterComponent } from "@curi/react-dom";

let Router = createRouterComponent(router);

ReactDOM.render((
  <ApolloProvider client={client}>
    <Router>
      <App />
    </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
let routes = prepareRoutes([
  {
    name: 'Noun',
    path: 'noun/:word',
    respond: () => {
      return {
        body: Noun
      };
    }
  }
]);

Any location data that a query needs can be taken from the response object. The best way to access this is to read the current response from the context. This can either be done in the component or the response can be passed down from the root app.

import { useResponse } from "@curi/react-dom";

function App() {
  let { response } = useResponse();
  let { body:Body } = response;
  return <Body response={response} />;
}

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";

let 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
let Noun = ({ response }) => (
  <Query
    query={GET_NOUN}
    variables={{ word: response.params.word }}
  >
    {({ loading, error, data }) => {
      if (loading) {
        return <Loading />;
      }
      // ...

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

Tight Pairing

You can use your Apollo client instance to call queries in a route's resolve function. resolve is expected to return a Promise, which is exactly what client.query returns. Tightly pairing Curi and Apollo is mostly center around using resolve 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";

let router = createRouter(browser, routes, {
  external: { client }
});
import { EXAMPLE_QUERY } from "./queries";

let routes = prepareRoutes([
  {
    name: "Example",
    path: "example/:id",
    resolve({ 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 respond 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({ params }, external) {
      return external.client.query({
        query: GET_VERB,
        variables: { word: params.word }
      });
    },
    respond({ 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
let Verb = ({ response }) => (
  <article>
    <h1>{response.data.verb.word}</h1>
    <Paragraph>
      {response.data.verb.definition}
    </Paragraph>
  </article>
)

The second approach is to use the 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 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({ 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";

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

Prefetching

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

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

// index.js
import { prefetch } from "@curi/router";

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

let router = createRouter(browser, routes);

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