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
method, uploading files with Relay Modern is a bit more involved.getFiles
Relay Modern expects an uploadables object that contains the file to be passed into
. Here is the React component that calls the mutation:commitMutation
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!