Uploading files with Relay Modern

Recently, I was trying to upload a file using a Relay Modern mutation. Having done this before in Relay Classic, I expected that it was going to be easy. But after just a little bit of googling, I found that it wasn’t.

It turns out that Relay Modern doesn’t have the built-in getFiles method of Relay Classic. In Relay Classic, you could just use this method in your mutation, and then access the file in your GraphQL controller using params[:file]. From there, you could add the file to the context object and use it in your GraphQL mutation. But without the getFiles method, uploading files with Relay Modern is a bit more involved.

Relay Modern expects an uploadables object that contains the file to be passed into commitMutation. Here is the React component that calls the mutation:

import React, { Component } from 'react'
import { UploadFileMutation } from './UploadFileMutation'

export class FileUploadForm extends Component {
  uploadFile = (event) => {
    const inputs = { userId: this.props.user.id }
    const uploadables = { file: event.target.files[0] }

    UploadFileMutation.commit(
      this.props.relay.environment,
      inputs,
      uploadables,
      this.handleCompleted,
      this.handleError
    )
  }

  render () {
    return (
      <form>
        <input type='file' id='file' onChange={this.uploadFile} />
      </form>
    )
  }
}

And here is the Relay Mutation:

import { commitMutation, graphql } from 'react-relay'

const mutation = graphql`
  mutation UploadFileMutation($input: UploadFileInput!) {
    uploadFile(input: $input) {
      file {
        id
      }
    }
  }
`

function commit(environment, input, uploadables, onCompleted, onError) {
  return commitMutation(environment, {
    mutation,
    variables: { input },
    uploadables,
    onCompleted,
    onError
  })
}

export default { commit }

Then, in your RelayEnvironment.js, you need to implement the actual upload yourself. This involves two main parts: creating a new FormData object that contains the file you want to upload, and making sure the Content-Type header is multipart/form-data  instead of application/json. You don’t need to explicitly set the Content-Type header for the upload request, but you do need to explicitly set it to application/json for all other requests.

Here is the RelayEnvironment.js:

import { Environment, Network, RecordSource, Store } from "relay-runtime"
import { isEmpty } from 'lodash'

function fetchQuery (operation, variables, cacheConfig, uploadables) {
  let requestVariables = {
    method: 'POST',
    headers: {
      'Accept': 'application/json'
    }
  }

  let body
  if (!isEmpty(uploadables)) {
    if (!window.FormData) {
        throw new Error('Uploading files without `FormData` not supported.')
    }

    const formData = new FormData()
    formData.append('query', operation.text)
    formData.append('variables', JSON.stringify(variables))

    Object.keys(uploadables).forEach(key => {
      if (Object.prototype.hasOwnProperty.call(uploadables, key)) {
        formData.append(key, uploadables[key])
      }
    })

    body = formData
  } else {
    requestVariables.headers['Content-Type'] = 'application/json'

    body = JSON.stringify({
      query: operation.text,
      variables
    })
  }

  return window.fetch(
    process.env.GRAPHQL_ENDPOINT,
    {
      ...requestVariables,
      body
    }
  ).then(response => response.json())
}

export default new Environment({
  network: Network.create(fetchQuery),
  store: new Store(new RecordSource())
})

You can then access the file in your GraphQL controller using params[:file], which you can add to the context object so that you can use it in your GraphQL mutation.

And that’s it! Thanks for reading, and I hope this helped!

Pooja is a member of DevMynd’s software engineering team focusing on mobile apps and web development. She’s been with the company since 2017.