Search code examples
graphqlgraphql-jsrelayjsrelay

Any way to split up multiple Fragment expansions for a GraphQL query into multiple calls?


Context

This problem is likely predicated on certain choices, some of which are changeable and some of which are not. We are using the following technologies and frameworks:

  • Relay / React / TypeScript
  • ContentStack (CMS)

Problem

I'm attempting to create a highly customizable page that can be built from multiple kinds of UI components based on the data presented to it (to allow pages to be built using a CMS using prefab UI in an unpredictable order).

My first attempt at this was to create a set of fragments for the potential UI components that may be referenced in an array:

query CustomPageQuery {
    title
    description
    customContentConnection {
        edges {
            node {
                ... HeroFragment
                ... TweetBlockFragment
                ... EmbeddedVideoFragment
                
                """
                Further fragments are added here as we add more kinds of UI  
                """
            }
        }
    }
}

In the CMS we're using (ContentStack), the complexity of this query has grown to the point that it is rejected because it requires too many calls to the database in a single query. For that reason, I'm hoping there's a way I can split up the calls for the fragments so that they are not part of the initial query, or some similar solution that results in splitting up this query into multiple pieces.

I was hoping the @defer directive would solve this for me, but it's not supported by relay-compiler.

Any ideas?


Solution

  • Sadly @defer is still not a standard so it is not supported by most implementation (since you would also need the server to support it).

    I am not sure if I understand the problem correctly, but you might want to look more toward using @skip or @include to only fetch the fragment you need depending on the type of the thing. But it would require the frontend to know what it wants to query beforehand.

    query CustomPageQuery($hero: Boolean, $tweet: Boolean, $video: Boolean) {
        title
        description
        customContentConnection {
            edges {
                node {
                    ... HeroFragment @include(if: $hero)
                    ... TweetBlockFragment @include(if: $tweet)
                    ... EmbeddedVideoFragment @include(if: $video)
                }
            }
        }
    }
    

    Generally you want to be able to discriminate the type without having to do a database query. So say:

    type Hero {
      id: ID
      name: String
    }
    type Tweet {
      id: ID
      content: String
    }
    union Content = Hero | Tweet
    
    {
      Content: {
        __resolveType: (parent, ctx) => {
          // That should be able to resolve the type without a DB query
        },
      }
    }
    

    Once that is passed, each fragment is then resolved, making more database queries. If those are not properly batched with dataloaders then you have a N+1 problem. I am not sure how much control (if at all) you have on the backend but there is no silver bullet for your problem.

    If you can't make optimizations on the backend then I would suggest trying to limit the connection. They seem to be using cursor based pagination, so you start with say first: 10 and once the first batch is returned, you can query the next elements by setting the after to the last cursor of the previous batch:

    query CustomPageQuery($after: String) {
        customContentConnection(first: 10, after: $after) {
            edges {
                cursor
                node {
                    ... HeroFragment
                    ... TweetBlockFragment
                    ... EmbeddedVideoFragment
                }
            }
            pageInfo {
              hasNextPage
            }
        }
    }
    

    As a last resort, you could try to first fetch all the IDs and then do subsequent queries to the CMS for each id (using aliases I guess) or type (if you can filter on the connection field). But I feel dirty just writing it so avoid it if you can.

    {
      one: node(id: "UUID1") {
        ... HeroFragment
        ... TweetBlockFragment
        ... EmbeddedVideoFragment
      }
      two: node(id: "UUID2") {
        ... HeroFragment
        ... TweetBlockFragment
        ... EmbeddedVideoFragment
      }
    }