last published March 1, 2018
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!



