Skip to content

Instantly share code, notes, and snippets.

@thomasboyt
Last active November 9, 2017 00:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomasboyt/ae8af67ae66c70fd0719324563dde37b to your computer and use it in GitHub Desktop.
Save thomasboyt/ae8af67ae66c70fd0719324563dde37b to your computer and use it in GitHub Desktop.
a graphql question

so, let's say I have a "master/detail"-ish architecture, where my UI has a section for a specific user, and within that section is a page that shows that user's followers. it's at a route like /users/:username/followers.

in my component hierarchy, this works with something like

<FollowersPage>         <-- top level route component, matched by react-router
  <UserSectionWrapper>  <-- renders the outer wrapper for the user section
    <FollowersList />   <-- the inner content of the page
  </UserSectionWrapper>
</FollowersPage>

<UserSectionWrapper /> renders some basic information about a user, such as their display name and avatar. <FollowersList /> renders the list of their followers.

Now, how I was hoping to structure this, using GraphQL, was to have <UserSectionWrapper /> wrapped with a higher-order GraphQL component:

const userQuery = gql`
  query User($username: String!) {
    user(username: $username) {
      id
      displayName
      avatar
    }
  }
`;

const withUser = graphql<any, Props>(userQuery, {
  options: (props: Props) => {
    return {
      variables: {
        username: props.username,
      },
    };
  },
});

And also wrap <FollowersList /> in a similar HOC:

const followersQuery = gql`
  query Followers($username: String!) {
    user(username: $username) {
      followers {
        id
        username
        displayName
      }
    }
  }
`;

const withFollowers = graphql<any, Props>(followersQuery, {
  options: (props: Props) => {
    return {
      variables: {
        username: props.username,
      },
    };
  },
});

However, the problem with this is that it makes a separate GraphQL request for each component. This is bad.

So, my assumption is that I need to move the queries into the top-level <FollowersPage/ >. Then, I can either pass down the data into my component tree, or continue using the HoCs as-written and just rely on the Apollo cache to avoid remaking the requests.

So, what's the best way to structure this? Can I write, like, a UserSectionWrapper query, and a FollowersList query, and then combine them into a FollowersPage query? Is this what "fragments" are for? The Apollo docs are confusing on this (I think partially because it assumes I have a greater understanding of GraphQL than I do).

@mergebandit
Copy link

mergebandit commented Nov 8, 2017

Yeah so how I handle this is to create a fragment which can be spread into the User query. I don't want the entirety of the user document within my followers, so I then use filter from graphql-anywhere to filter the results of the User query based on my fragment. We actually do this filtering in a higher-order component and use recompose to ultimately pass the data down to Followers - such that props.followers is hydrated outside of the component.

gql/fragments/followers.js

export default gql`
fragment FollowersFragment on User {
  followers{
    id
    username
    displayName
  }
}`

gql/queries/user.js

import FollowersFragment from './gql/fragments/followers.js'

const userQuery = gql`
  query User($username: String!) {
    user(username: $username) {
      id
      displayName
      avatar
      ...FollowersFragment
    }
  }
  ${FollowersFragment}
`;

components/User/index.js

import Followers from 'components/Followers'

// assuming that this User component is wrapped in the `graphql(UserQuery, options)` HoC
// we feed every response into a `withErrorAndLoading` HoC to handle `data.error` and `data.loading` states
const User = props => (
  <Followers user={props.data.user} />
)

components/Followers/index.js

import { filter } from 'graphql-anywhere'
import FollowersFragment from './gql/fragments/followers.js'

const Followers = props => {
  const followers = filter(FollowersFragment, props.user)
  return (...)
}

@thomasboyt
Copy link
Author

followup question on caching:

what's up with apollo's cache operating at the "query" level, and not at, like, an entity level? it kinda makes my plan of "the top-level component provides the entire page's data" worse when now, when navigating between pages, it always has to re-query all data, since there is no shared cached knowledge.

as a concrete example, given my previous gist: if I have a /users/thomas/followers page, and a /users/thomas/following page, and they each have these queries:

query FollowersPage($username: String!) {
  user(username: $username) {
    id
    displayName
    avatar
    followers {
      id
      username
      displayName
    }
  }
}
query FollowingPage($username: String!) {
  user(username: $username) {
    id
    displayName
    avatar
    following {
      id
      username
      displayName
    }
  }
}

When I navigate from /followers to /following, Apollo doesn't know that the id/displayName/avatar are the same, and re-queries them.

this isn't a huge deal from a performance perspective, but is bad from a UI perspective: when I navigate from /followers to /following, I want to continue showing a UI using state about the user while loading the next screen, and currently, I have no way to access that state while loading, since it's not cached under the same query.

@mergebandit
Copy link

If you query for data that's already been fetched - even in a separate / distinct query, it will pull it from the cache first. If it's not in the cache, then it'll make the network request. Are you seeing something different?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment