We're using GraphQL a lot, and due to it's reusability nature, we often pair it with React.js and React Native applications.
However, one challenge we faced was file uploads. GraphQL doesn't come with a built-in way to handle file uploads. While solutions like Apollo Upload Server exist, but we wanted to explore Active Storage's direct upload as an official solution.
What is Direct Upload?
In a normal (non-direct) file upload, the file is first sent to the Rails server, which then processes and uploads it to cloud storage (such as S3, Azure, Google cloud storage).
In Direct Upload, files go straight from the browser to cloud storage, which improves performances and reduces server load.
Direct upload is not limited only to the cloud storages mentioned above, You can even upload to server's local disk, in order to keep things simple.
In this guide, we'll be using React.js, Ant Design, Ruby GraphQL and direct upload to rails's local disk.
1. Integrate direct upload with form component
Rails has a built-in endpoint for direct uploads at
/rails/active_storage/direct_uploads
. You can check out the controller source
code
here.
Let's define a helper function for file uploads:
// src/api/activeStorage.ts
import { Blob, DirectUpload } from "@rails/activestorage";
const BASE_URL = "https://example.com/rails/active_storage/direct_uploads";
const directUploadCreate = (file: File) => {
const upload = new DirectUpload(file, BASE_URL);
return new Promise<Blob>((resolve, reject) => {
upload.create((err, blob) => {
if (err) {
reject(err.message);
} else {
resolve(blob);
}
});
});
};
When this function is called with a selected file, it returns a signed_id
string, which acts as a placeholder for the uploaded file. Since it's just a
string, calling the mutation becomes much simpler compared to using
ApolloUploadServer's file type.
You can now treat avatar
like a regular string field and update your GraphQL
schema accordingly to accept it as a string.
Now, let's integrate above function with Ant Design's form component:
// src/pages/user-form.tsx
const UserForm = () => {
return (
...
<Form.Item>
<Upload
customRequest={async ({ file }) => {
form.setFieldsValue({
avatar: (await directUploadCreate(file)).signed_id
});
}}
>
<Button>Upload</Button>
</Upload>
</Form.Item>
...
);
};
2. Update GraphQL mutation
Update GraphQL schema to accept avatar
as a string field:
# app/graphql/types/user_attributes.rb
class Types::UserAttributes < Types::BaseInputObject
argument :avatar, String, required: false
# ...
end
Create user update mutation:
# app/graphql/mutations/user_update.rb
class Mutations::UserUpdate < Mutations::BaseMutation
argument :attributes, Types::UserAttributes, required: true
type Types::UserType, null: false
def resolve(attributes:)
user = context[:current_user]
if !user.update(**attributes)
raise GraphQL::ExecutionError, user.errors.full_messages.to_sentence
end
user
end
end
Add mutation to schema:
# app/graphql/types/mutation_type.rb
class Types::MutationType < Types::BaseObject
field :user_update, mutation: Mutations::UserUpdate, null: false
end
3. Call mutation from client
To trigger GraphQL mutation on form submission, we'll use graffle to send the request from our React app:
// src/api.ts
import { gql } from "graffle";
export const userUpdateDocument = gql`
mutation userUpdate($input: UserUpdateInput!) {
userUpdate(input: $input) {
avatar
id
}
}
`;
This is how complete integration looks:
// src/pages/user-form.tsx
import { request } from "graffle";
const UserForm = () => {
const [form] = Form.useForm();
return (
<Form
form={form}
onFinish={(attributes) => {
request(userUpdateDocument, { input: { attributes } });
}}
>
<Form.Item>
<Upload
customRequest={async ({ file }) => {
form.setFieldsValue({
avatar: (await directUploadCreate(file)).signed_id,
});
}}
>
<Button>Upload</Button>
</Upload>
</Form.Item>
</Form>
);
};
That's it!
If you found this post useful, don't miss our latest tech posts. Keep innovating!