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?
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.
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.
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?
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With