In the previous post I wired up a React Native client to a Rails API GraphQL server. In this post I’ll enable adding, removing and retrieving paginated data.
Data Models
The root of all my GraphQL queries are now a user
and a user has a number of meetings
. A user can sign-up, then meetings can be created or destroyed via mutations. See 33-minutes-server@838200 for implementation details.
Relay-Style Mutations
Relay has a specified add/remove behavior via RANGE_ADD
and NODE_DELETE
.
RANGE_ADD
To enable this on the server, return the range connection and edge from the mutation.
Mutations::CreateMeetingMutation = GraphQL::Relay::Mutation.define do
name 'createMeeting'
input_field :title, types.String
input_field :started, !Types::DateTimeType
input_field :finished, !Types::DateTimeType
return_field :meeting, Types::MeetingType
return_field :meetingsConnection, Types::MeetingType.connection_type
return_field :meetingEdge, Types::MeetingType.edge_type
resolve ->(_object, inputs, ctx) {
user = ctx[:current_user]
meeting = user.meetings.create!(
title: inputs[:title],
started_at: inputs[:started],
finished_at: inputs[:finished]
)
range_add = GraphQL::Relay::RangeAdd.new(
parent: user,
collection: user.meetings,
item: meeting,
context: ctx
)
{
meeting: meeting,
meetingsConnection: range_add.connection,
meetingEdge: range_add.edge
}
}
end
The client-side mutation needs a parent ID (a user ID), has to request the edge, and include RANGE_ADD
in its configs.
import { graphql } from 'react-relay'
import commitMutation from 'relay-commit-mutation-promise'
const mutation = graphql`
mutation CreateMeetingMutation($input: createMeetingInput!) {
createMeeting(input: $input) {
meeting {
id
title
started
finished
},
meetingEdge {
node {
id
}
}
}
}
`
function commit(userId, { environment, input }) {
const variables = { input }
return commitMutation(environment, {
mutation,
variables,
configs: [{
type: 'RANGE_ADD',
parentID: userId,
connectionInfo: [{
key: 'Meetings_meetings',
rangeBehavior: 'append',
}],
edgeName: 'meetingEdge'
}]
})
}
export default {
commit
}
You can see the complete server-side code in 33-minutes-server@2a70b7 and client-side code in 33-minutes-app@f253e4.
NODE_DELETE
The server has to return deletedId
.
Mutations::DeleteMeetingMutation = GraphQL::Relay::Mutation.define do
name 'deleteMeeting'
input_field :id, !types.ID
return_field :deletedId, !types.ID
resolve ->(_object, inputs, ctx) {
user = ctx[:current_user]
meeting = user.meetings.find(inputs[:id])
meeting.destroy!
{
deletedId: meeting.id
}
}
end
The client-side mutation needs the parent ID (a user ID) and to include a NODE_DELETE
in its configs.
import { graphql } from 'react-relay'
import commitMutation from 'relay-commit-mutation-promise'
const mutation = graphql`
mutation DeleteMeetingMutation($input: deleteMeetingInput!) {
deleteMeeting(input: $input) {
deletedId
}
}
`
function commit(userId, { environment, input }) {
const variables = { input }
return commitMutation(environment, {
mutation,
variables,
configs: [{
type: 'NODE_DELETE',
parentID: userId,
deletedIDFieldName: 'deletedId',
connectionName: 'Meetings_meetings'
}]
})
}
export default {
commit
}
Note that NODE_DELETE
empties a node in the local store, but doesn’t remove it. Therefore a if (node)
is needed in the list renderer. This is relay#2155.
let meetings = this.props.user.meetings.edges.map(({node}) => {
if (node) {
return <Meeting key={node.__id} meeting={node} deleteMethod={ () => this.removeMeetingById(node.__id) } />
}
})
You can see the complete server-side code in 33-minutes-server@11c324 and client-side code in 33-minutes-app@f253e4.
Relay-Style Connections
Relay’s support for pagination relies on the GraphQL server exposing connections in a standardized way. To expose user’s meetings as a GraphQL field you would write field :meetings, -> { !types[Types::MeetingType] }
. To enable this to be Relay-style, use connection
.
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, types.ID, 'User ID.'
field :name, types.String, 'User name.'
connection :meetings, Types::MeetingType.connection_type
end
This returns meetings with edge
and node
elements along with pageInfo
and much more.
See 33-minutes-server@f0bd7d for a complete implementation. You will also need rmosolgo/graphql-ruby#1754 to make this work with Mongoid.
To enable this on the client requires a lot of boilerplate, including a pagination container and a call to Relay to load more data as documented. I suggest just copy-pasting and adapting working code from 33-minutes-app@4fd9bd.