apollo-client: How to get inverse relation from cache?

875 views Asked by At

I have a graphql query response of the shape

{
  table {
    id
    legs {
      id
    }
  }

This normalizes to table and leg entries in my InMemoryCache.

But then if my application retrieves a leg from cache and needs to know the corresponding table, how would I find this?

The two ideas I had are

  • adding a table prop to each leg when the query response comes in - not sure if/how that would work (I have multiple queries and mutations containing the graphql fragment with above shape)
  • having a suitable cache redirect, but I don't know how to do this without searching all tables for the leg.

Does apollo provide any features suitable to achieve this inverse lookup?

Update: to clarify, the leg has a table prop, but I since I already have the info in the client before resolving that prop, I'd like to resolve that prop client-side instead of server-side.

1

There are 1 answers

10
Clément Prévost On

You should be adding a table prop to each leg. According to the graphql.org documentation you should be thinking in graphs:

With GraphQL, you model your business domain as a graph by defining a schema; within your schema, you define different types of nodes and how they connect/relate to one another.

In your model, tables and legs are nodes in your business model graph. When you add a table prop to each leg you are creating a new edge in this graph that your client-side code can traverse to get the relevant data.

Edit after clarification:

You can use writeFragment and to gain fine grained control of the Apollo cache. Once the cache filling query is done, compute the inverse relationship and write it to the cache like so:

fetchTables = async () => {
  const client = this.props.client

  const result = await client.query({
    query: ALL_TABLES_QUERY,
    variables: {}
  })

  // compute the reverse link
  const tablesByLeg = {}
  for (const table of result.data.table) {
    for (const leg of table.legs) {
      if (!tablesByLeg[leg.id]) {
        tablesByLeg[leg.id] = {
          leg: leg,
          tables: []
        }
      }
      tablesByLeg[leg.id].tables.push(table)
    }
  }

  // write to the Apollo cache
  for (const { leg, tables } of Object.values(tablesByLeg)) {
    client.writeFragment({
      id: dataIdFromObject(leg),
      fragment: gql`
        fragment reverseLink from Leg {
          id
          tables {
            id
          }
        }
      `,
      data: {
        ...leg,
        tables
      }
    })
  }

  // update component state
  this.setState(state => ({
    ...state,
    tables: Object.values(result)
  }))
}

Demo

I put up a complete exemple here: https://codesandbox.io/s/6vx0m346z I also put it below just for completeness sake.

index.js

import React from "react";
import ReactDOM from "react-dom";
import { ApolloProvider } from "react-apollo";
import { createClient } from "./client";
import { Films } from "./Films";

const client = createClient();

function App() {
  return (
    <ApolloProvider client={client}>
      <Films />
    </ApolloProvider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

client.js

import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";

export function dataIdFromObject(object) {
  return object.id ? object.__typename + ":" + object.id : null;
}

export function createClient() {
  return new ApolloClient({
    connectToDevTools: true,
    ssrMode: false,
    link: new HttpLink({
      uri: "https://prevostc-swapi-graphql.herokuapp.com"
    }),
    cache: new InMemoryCache({
      dataIdFromObject,
      cacheRedirects: {
        Query: {
          planet: (_, args, { getCacheKey }) =>
            getCacheKey({ __typename: "Planet", id: args.id })
        }
      }
    })
  });
}

Films.js

import React from "react";
import gql from "graphql-tag";
import { withApollo } from "react-apollo";
import { dataIdFromObject } from "../src/client";
import { Planet } from "./Planet";

const ALL_FILMS_QUERY = gql`
  query {
    allFilms {
      films {
        id
        title
        planetConnection {
          planets {
            id
            name
          }
        }
      }
    }
  }
`;

const REVERSE_LINK_FRAGMENT = gql`
  fragment reverseLink on Planet {
    id
    name
    filmConnection {
      films {
        id
        title
      }
    }
  }
`;

class FilmsComponent extends React.Component {
  constructor() {
    super();
    this.state = { films: [], selectedPlanetId: null };
  }

  componentDidMount() {
    this.fetchFilms();
  }

  fetchFilms = async () => {
    const result = await this.props.client.query({
      query: ALL_FILMS_QUERY,
      variables: {}
    });

    // compute the reverse link
    const filmByPlanet = {};
    for (const film of result.data.allFilms.films) {
      for (const planet of film.planetConnection.planets) {
        if (!filmByPlanet[planet.id]) {
          filmByPlanet[planet.id] = {
            planet: planet,
            films: []
          };
        }
        filmByPlanet[planet.id].films.push(film);
      }
    }

    // write to the apollo cache
    for (const { planet, films } of Object.values(filmByPlanet)) {
      this.props.client.writeFragment({
        id: dataIdFromObject(planet),
        fragment: REVERSE_LINK_FRAGMENT,
        data: {
          ...planet,
          filmConnection: {
            films,
            __typename: "PlanetsFilmsConnection"
          }
        }
      });
    }

    // update component state at last
    this.setState(state => ({
      ...state,
      films: Object.values(result.data.allFilms.films)
    }));
  };

  render() {
    return (
      <div>
        {this.state.selectedPlanetId && (
          <div>
            <h1>Planet query result</h1>
            <Planet id={this.state.selectedPlanetId} />
          </div>
        )}
        <h1>All films</h1>
        {this.state.films.map(f => {
          return (
            <ul key={f.id}>
              <li>id: {f.id}</li>
              <li>
                title: <strong>{f.title}</strong>
              </li>
              <li>__typename: {f.__typename}</li>
              <li>
                planets:
                {f.planetConnection.planets.map(p => {
                  return (
                    <ul key={p.id}>
                      <li>id: {p.id}</li>
                      <li>
                        name: <strong>{p.name}</strong>
                      </li>
                      <li>__typename: {p.__typename}</li>
                      <li>
                        <button
                          onClick={() =>
                            this.setState(state => ({
                              ...state,
                              selectedPlanetId: p.id
                            }))
                          }
                        >
                          select
                        </button>
                      </li>
                      <li>&nbsp;</li>
                    </ul>
                  );
                })}
              </li>
            </ul>
          );
        })}
        <h1>The current cache is:</h1>
        <pre>{JSON.stringify(this.props.client.extract(), null, 2)}</pre>
      </div>
    );
  }
}

export const Films = withApollo(FilmsComponent);

Planet.js

import React from "react";
import gql from "graphql-tag";
import { Query } from "react-apollo";

const PLANET_QUERY = gql`
  query ($id: ID!) {
    planet(id: $id) {
      id
      name
      filmConnection {
        films {
          id
          title
        }
      }
    }
  }
`;

export function Planet({ id }) {
  return (
    <Query query={PLANET_QUERY} variables={{ id }}>
      {({ loading, error, data }) => {
        if (loading) return "Loading...";
        if (error) return `Error! ${error.message}`;

        const p = data.planet;
        return (
          <ul key={p.id}>
            <li>id: {p.id}</li>
            <li>
              name: <strong>{p.name}</strong>
            </li>
            <li>__typename: {p.__typename}</li>
            {p.filmConnection.films.map(f => {
              return (
                <ul key={f.id}>
                  <li>id: {f.id}</li>
                  <li>
                    title: <strong>{f.title}</strong>
                  </li>
                  <li>__typename: {f.__typename}</li>
                  <li>&nbsp;</li>
                </ul>
              );
            })}
          </ul>
        );
      }}
    </Query>
  );
}