Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

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.