Multiple Lists w/ Same Query

I have a query for searching and sorting on a list of things. Let say they are Projects (example query below). I have several instances of a ProjectListComponent (react) in my app that are each a different view into projects (MyProjects, LatestProjects, ProjectSearch, etc). Several of them can have active queries at the same time. The number of search and sort permutations is large, in the thousands. There are also potentially thousands of Projects in the data source. They can grow unbounded. I do not want to use the search and sorting args as cache keys, as I feel there are too many permutations. What I think I want is a cached list (cacheKey for the returned list. in the example: searchProjects) that represents each ProjectListComponent UI Instance.

One solution, albeit hacky, is to add a new variable to the searchProjects query that is ignored by the back end and use that as my cache key along with a fetchPolicy of cache-and-network.

I was also looking for a way to send some context with a query that I could use in a keyArgs function to generate an appropriate cache key for the returned list. Nothing seemed appropriate, but I may be missing something.

This seems like a fairly common setup (I hav a few instances in the code base I’m working on) and I’m looking for suggestions on how to solve this (gracefully, if possible). Any help would be greatly appreciated.

Query:

query searchProjects(
  $filters: ProjectFilters
  $sortBy: ProjectSortBy
  $first: Int
  $after: String
) {
  searchProjects(
    filters: $filters
    sortBy: $sortBy
    first: $first
    after: $after
  ) {
    edges {
      cursor
      node {
        ...ProjectListItemFragment
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
  }
}

Hey @asymptotik :wave:

I’d recommend checking out the @connection directive which you can use to determine a UI-specific cache key for the list. You can configure your key arguments to use the key arg to the @connection directive. Here is an example:

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        searchProjects: {
          keyArgs: ['@connection', ['key']],
        }
      }
    }
  }
})

Then in your query, use the directive:

query {
  searchProjects(...) @connection(key: "UIElement1")
}

If you want to see an example in an app, check out our Spotify showcase which has this pattern. Here is a sample query that uses @connection along with the field policy that uses the @connection key.

Hope this helps!

Thank you jerelmille, for the quick reply. I did a little proof of concept and this works. However, it seems to require that I create a new query for each key. Is it possible to set the key value dynamically? I tried something like the query below, but when calling it via useQuery and passing in the key value, I get an error on the server “error: apolloErrorHandler Error: Variable “$key” is never used in operation “listProjects””.

It appears @client is not honored for query variables. I’m not completely sure what I attempted makes sense, but it was the first thing that came to mind.

The useQuery, in my code, is embedded in another hook that queries the data, transforms the data and does a little filtering that is not done on the back end. It would be really nice to be able to just pass the key value to that hook. That hook will be used in quite a few places. Including a column in a dynamic set of columns that will all be on the screen at once.

Is there any way of doing something like this?

query listProjects(
  $key: String! @client
  $orgId: ID!
  $filters: ListProjectsFilters
  $sortBy: ListProjectsSortBy
  $first: Int
  $after: String
) {
  listProjects(
    orgId: $orgId
    filters: $filters
    sortBy: $sortBy
    first: $first
    after: $after
  ) @connection(key: $key) {
    ...ListProjectsFragment
  }
}

Ah ya unfortunately we have a bug when using a variable with @connection right now that you’re probably running into :disappointed: Passing query variables to Apollo @connection directive key · Issue #11099 · apollographql/apollo-client · GitHub. What you’re asking for SHOULD work, but the variable isn’t stripped before the query is sent to the client, so the server complains. @client won’t work in arguments as its meant for fields themselves. Let me noodle on this a bit more to see if I can come up with a reasonable workaround for now.

Awesome! Thank you. And thanks for the link to the bug. I have subscribed to it.

I think I’m going to hack it a bit. I have access to the back end and can just add a dummy argument just for the cache key purpose. If something changes, I can just update the code.

That works! A (more challenging) temporary workaround you might also consider is creating a custom link that “catches” unused variables in the query document and removes them before sending it through the link chain. This way you won’t have to modify your backend.

As a starting point:

const removeUnusedVariablesLink = new ApolloLink((operation, forward) => {
  operation.query = removeUnusedVariables(operation.query)

  return forward(operation);
})

The tricky part is implementing that removeUnusedVariables helper function. We do export a removeArgumentsFromDocument helper function with the library that gets you part of the way, but it needs the names of the arguments you want to remove. To use it

import { removeArgumentsFromDocument } from '@apollo/client/utilities';

const modifiedQuery = removeArgumentsFromDocument([{ name: 'varName' }], query)

One way to get the list of unused variables is to traverse the query document with the visit function from the graphql package (a bit challenging).

An absolute quick and dirty hack without needing to do that would be to pass through the connection arg in context, then use that to call removeArgumentsFromDocument. That might look something like this:

const removeConnectionArgLink = new ApolloLink((operation, forward) => {
  const { connectionVariables } = operation.getContext();

  if (!connectionVariables) {
    return forward(operation);
  }

  const config = connectionVariables.map(name => ({ name }));
  operation.query = removeArgumentsFromDocument(config, operation.query);

  return forward(operation);
});

Then use it as such:

const query = gql`
  query($key: String) {
    listProjects @connection(key: $key)  { 
      # ...
    } 
  }  
`

useQuery(query, { context: { connectionVariables: ['key'] })

Again, terribly hacky, but might give you a good starting point :slightly_smiling_face:

Oh man, I love that you really dug in here, which helps not only solve my issue, but gives me some insight into areas I’ve not explored. Thank you! This is super helpful and interesting and will probably help the next person that comes along without access to the back end.

2 Likes