Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Organization structure for fragment composition in large react-apollo apps

I'm using Apollo Client and React and I'm looking for a strategy to keep my component and component data requirements colocated in such a way that it can be accessible to parent/sibling/child components that might need it for queries and mutations. I want to be able to easily update the data requirements which in turn will update the fields that are queried by some parent component or returned by a mutation in a parent/sibling/child in order to accurately update my Apollo cache.

I have tried creating a global high level graphql directory where all my queries/mutations.graphql files are located, importing all the related fragment files located throughout my app, and then importing those directly, but this can get tedious and doesn't follow the parent/child theme where parent queries include children fragments. Also in large projects you end up traversing long file paths when importing.

I have also tried just creating fragment files colocated in the global graphql directory that correspond to component files but this doesn't give me the "component/data requirement" colocation I'm looking for.

This works:

class CommentListItem extends Component {
  static fragments = {
    comment: gql`
      #...
    `,
  }
}
class CommentList extends Component {
  static fragments = {
    comment: gql`
      #...
      ${CommentListItem.fragments.comment}
    `,
  }
}
class CommentsPage extends Component {
  static fragments = {
    comment: gql`
      #...
      ${CommentList.fragments.comment}
    `,
  }
}
graphql(gql`
  query Comments {
    comments {
      ...CommentsListItemComment
    }
  }
  ${CommentsPage.fragments.comment}
`)

However, if I want a mutation in a descendent of CommentsPage I can't reference the fragment composition from CommentsPage.fragments.comment.

Is there a preferred method or best practice for this type of thing?

like image 575
Donovan Hiland Avatar asked Dec 29 '17 03:12

Donovan Hiland


Video Answer


1 Answers

Structuring Queries

How to structure your code is always a matter of a personal taste but I think the collocation of queries and components is a big strength of GraphQL.

For queries I took a lot of inspiration from Relay Modern and the solution looks very close to what you described in the code. Right now as the project becomes bigger and we want to generate Flow type definitions for our queries, putting them into separate files next to the component files is also an option. This will be very similar to CSS-modules.

Structuring Mutations

When it comes to mutations it often gets much harder to find a good place for them. Mutations need to be called on events far down the component tree and often change the state of the application in multiple states of the app. In this case you want the caller to be unaware of the data consumers. Using fragments might seem like an easy answer. The mutation would just include all fragments that are defined for a specific type. While the mutation now does not need to know which fields are required it needs to know who requires fields on the type. I want to point out two slightly different approaches that you can use to base your design on.

Global Mutations: The Relay Approach

In Relay Modern Mutations are basically global operations, that can be triggered by any component. This approach is not to bad since most mutations are only written once and thanks to variables are very reusable. They operate on one global state and don't care about which UI part consumes the update. When defining a mutation result you should usually query the properties that might have changed by the mutation instead of all the properties that are required by other components (through fragments). E.g. the mutation likeComment(id: ID!) should probably query for the likeCount and likes field on comment and not care much if any component uses the field at all or what other fields components require on Comment. This approach gets a bit more difficult when you have to update other queries or fields. the mutation createComment(comment: CreateCommentInput) might want to write to the root query object's comments field. This is where Relays structure of nodes and edges comes in handy. You can learn more about Relay updates here.

# A reusable likeComment mutation
mutation likeComment($id: ID!) {
  likeComment(id: $id) {
    comment {
      id
      likeCount
      likes {
        id
        liker {
          id
          name
        }
      }
    }
  }
}

Unfortunately we cannot answer one question: How far should we go? Do I need the names of the people liking the comments or does the component simply display a number of likes?

Mutations in Query Container

Not all GraphQL APIs are structured the Relay way. Furthermore Apollo binds mutations to the store similar to Redux action creators. My current approach is to have mutations on the same level as queries and then passing them down. This way you can access the children's fragments and use them in the mutations if needed. In your example the CommentListItem component might display a like button. It would define a fragment for the data dependencies, prop types according to the fragment and a function prop type likeComment: (id: string) => Promise<any>. This prop type would be passed through to the query container that wraps the CommentsPage in a query and mutation.

Summary

You can use both approaches with Apollo. A global mutations folder can contain mutations that can be used anywhere. You can then directly bind the mutations to the components that need them. One benefit is that e.g. in the likeComment example the variable id can be directly derived from the components props and does not need to be bound within the component itself. Alternatively you can pass mutations through from you query components. This gives you a broader overview of the consumers of data. In the CommentsPage it can be easier to decide what needs to be updated when a mutation completed.

Let me know what you think in the comments!

like image 74
Herku Avatar answered Nov 09 '22 13:11

Herku