File uploads with React and apollo (Part 2).
A complete guide on how to upload files to graphql server with react and apollo-upload-client.#
Bonus: You will also learn how to serve files from your apollo server with express.
Prerequisites#
- Knowledge of React
- Basic knowledge of Apollo
- Graphql API with file upload capabilities (Here is a complete guide on how to create Upload files on apollo-server)
Here is a demo of what we are going to build.
Let's get started 🚀#
First we are going to use the create-react-app
cli to bootstrap a new react
project by running:
npx create-react-app react-apollo-upload# oryarn create react-app react-apollo-upload# Change directory into react-apollo-upload by runningcd react-apollo-upload
Open the project in your favourite editor/IDE. I'll be using vs-code which is my favourite editor.
We are going to install all the required packages for now then i'll explain the function of each package.
npm install graphql graphql-tag apollo-upload-client @apollo/react-hooks apollo-cache-inmemory react-dropzone
Next thing is to setup our react application to be able use apollo-upload-client
so we are going to make few changes to our src/index.js
to look like:
import React from 'react';import ReactDOM from 'react-dom';import './index.css';import App from './App.jsx';import ApolloClient from 'apollo-client';import { createUploadLink } from 'apollo-upload-client';import { ApolloProvider } from '@apollo/react-hooks';import { InMemoryCache } from 'apollo-cache-inmemory';const httpLink = createUploadLink({uri: 'http://localhost:4000',});const client = new ApolloClient({link: httpLink,cache: new InMemoryCache(),});ReactDOM.render(<ApolloProvider client={client}><App /></ApolloProvider>,document.getElementById('root'),);
The traditional apollo-client react application uses apollo-link-http.
Apollo-link-http is a terminating link that fetches GraphQL results from a GraphQL endpoint over an http connection. The http link supports both POST and GET requests with the ability to change the http options on a per query basis. This can be used for authentication, persisted queries, dynamic uris, and other granular updates.
However, apollo-link-http doesn't support file uploads thats we we are going to use apollo-upload-client.
We created our upload link and stored it in a variable called httpLink then
we used the link as an option in the ApolloClient option. we also added
apollo-cache-inmemory for caching. then we wrap our <App/>
component with
ApolloProvider and pass in the client prop and now our entire application has
access to the apollo client we created.
For the purpose of code readability we are going to split our code into different components and they're going to live in the src/components directory.
Create an upload.jsx file in your src/components and add the following code which i will explain to you in a sec.
import React, { useCallback } from 'react';import { useDropzone } from 'react-dropzone';const FileUpload = () => {const onDrop = useCallback((acceptedFiles) => {// do something hereconsole.log(acceptedFiles);}, []);const { getRootProps, getInputProps, isDragActive } = useDropzone({onDrop,});return (<><div{...getRootProps()}className={`dropzone ${isDragActive && 'isActive'}`}><input {...getInputProps()} />{isDragActive ? (<p>Drop the files here ...</p>) : (<p>Drag 'n' drop some files here, or click to select files</p>)}</div></>);};export default FileUpload;
In the above code, we imported useCallback hook from react and useDropzone hook form react-dropzone. Next we destructured getRootProps, getInputProps, and isDragActive from useDropzone and we passed an onDrop callback as an option.
The useDropzone hook contains a lot of props which you can learn more about in there official github repo https://github.com/react-dropzone/react-dropzone/
Next we spread ...getRootProps() in our wrapper div and ...getInputProps() in the default html input element and react-dropzone will handle the rest for us.
We can perform a lot of operations in the onDrop callback. However, I'm just going to console.log the file for now to see what it looks like.
To test this out we need to import our component into the App.js component so your src/App.js should look like
import React from 'react';import logo from './logo.svg';import './App.css';import FileUpload from './components/upload';function App() {return (<div className='App'><header className='App-header'><img src={logo} className='App-logo' alt='logo' /><h1>Upload files effortlessly</h1></header><div className='container'><FileUpload /></div></div>);}export default App;
As we can see from the image above we get an array of files from react-dropzone. However, we only care about a single file because our server is currently configured to only accept a single file so we are going to use the first file by accessing its index which is at 0.
We are going to create our mutation and the graphql-tag package we installed enables us to do that.
...import gql from 'graphql-tag';const UploadMutation = gql`mutation uploadFile($file: Upload!) {uploadFile(file: $file) {pathidfilenamemimetype}}`;...
First we imported gql from graphql-tag then we create our Upload mutation which has a parameter file (in graphql, variables are written with a dollar sign prefix followed by the name) and its value is a graphql scaler type Upload.
...// import usemutation hook from @pollo/react-hooksimport { useMutation } from '@apollo/react-hooks';...// pass in the UploadMutation mutation we created earlier.const [uploadFile] = useMutation(UploadMutation);const onDrop = useCallback((acceptedFiles) => {// select the first file from the Array of filesconst file = acceptedFiles[0];// use the uploadFile variable created earlieruploadFile({// use the variables option so that you can pass in the file we got abovevariables: { file },onCompleted: () => {},});},// pass in uploadFile as a dependency[uploadFile]);...
Finally, your src/components/upload.js should look like
import React, { useCallback } from 'react';import { useDropzone } from 'react-dropzone';import { useMutation } from '@apollo/react-hooks';import gql from 'graphql-tag';const UploadMutation = gql`mutation uploadFile($file: Upload!) {uploadFile(file: $file) {pathidfilenamemimetype}}`;// pass in the UploadMutation mutation we created earlier.const FileUpload = () => {const [uploadFile] = useMutation(UploadMutation);const onDrop = useCallback((acceptedFiles) => {// select the first file from the Array of filesconst file = acceptedFiles[0];// use the uploadFile variable created earlieruploadFile({// use the variables option so that you can pass in the file we got abovevariables: { file },onCompleted: () => {},});},// pass in uploadFile as a dependency[uploadFile],);const { getRootProps, getInputProps, isDragActive } = useDropzone({onDrop,});return (<><div{...getRootProps()}className={`dropzone ${isDragActive && 'isActive'}`}><input {...getInputProps()} />{isDragActive ? (<p>Drop the files here ...</p>) : (<p>Drag 'n' drop some files here, or click to select files</p>)}</div></>);};export default FileUpload;
And thats all you need to upload files with apollo-upload-client and react. However, You are going to run into issues when trying to display files like images on the client side of your application but don't worry because that's what we are going to work on next.
BONUS 💃#
Henceforth, i'm just going to give you a brief walk through on how these code works and you can find the complete source code for both the server and client on github.
- server https://github.com/DNature/apollo-upload/tree/apollo-server-express
- client https://github.com/DNature/apollo-upload-client
Server#
Now we are going to configure our server to be able to serve static files so we are going to switch from the regular apollo-server to apollo-server-express.
Install express, cors and apollo-server-express by running
npm install cors express apollo-server-express
CORS: Cross-origin resource sharing is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served. A web page may freely embed cross-origin images, stylesheets, scripts, iframes, and videos. Wikipedia
I think this funny image explains cors better:
Add the following piece of code to make your server look like this
import { ApolloServer } from 'apollo-server-express';import typeDefs from './typeDefs';import resolvers from './resolvers';import express from 'express';import cors from 'cors'; // import corsimport path from 'path';const app = express();// Import your database configurationimport connect from './db';export default (async function () {try {await connect.then(() => {console.log('Connected 🚀 To MongoDB Successfully');});const server = new ApolloServer({typeDefs,resolvers,});const dir = path.join(process.cwd(), 'images');app.use('/images', express.static(dir)); // serve all files in the /images directoryapp.use(cors('*')); // All Cross-origin resource sharing from any networkserver.applyMiddleware({ app }); // apply express as a graphql middleware// server.listen(4000, () => {app.listen(4000, () => {console.log(`🚀 server running @ http://localhost:4000`);});} catch (err) {console.error(err);}})();
Client#
We are going to do two things on the client.
- Display files from the server,
- Create a new upload drop-zone that displays file preview.
Add a proxy that points to your server's domain in your package.json file.
{..."proxy": "http://localhost:4000/"}
Our server no longer uses apollo-server but uses apollo-server-express and the default endpoint of apollo-server-express is /graphql so we need to add that to our createUploadLink uri.
Now your src/index.js
should look like this
import React from 'react';import ReactDOM from 'react-dom';import './index.css';import App from './App.jsx';import ApolloClient from 'apollo-client';import { createUploadLink } from 'apollo-upload-client';import { ApolloProvider } from '@apollo/react-hooks';import { InMemoryCache } from 'apollo-cache-inmemory';const httpLink = createUploadLink({uri: 'http://localhost:4000/graphql',});const client = new ApolloClient({link: httpLink,cache: new InMemoryCache(),});ReactDOM.render(<ApolloProvider client={client}><App /></ApolloProvider>,document.getElementById('root'),);
Crate a file and name it Uploads.js
in your src/components directory then
add the following code:
import React from 'react';import { useQuery } from '@apollo/react-hooks'; // import useQuery hookimport gql from 'graphql-tag';// FilesQueryexport const FileQuery = gql`{files {idfilenamemimetypepath}}`;export default function Uploads() {const { loading, data } = useQuery(FileQuery,); /* useQuery returns and object with **loading,data, and error** but we only care about the loading state and the data object.*/if (loading) {// display loading when files are being loadedreturn <h1>Loading...</h1>;} else if (!data) {return <h1>No images to show</h1>;} else {return (<><h1 className='text-center'>Recent uploads</h1>{data.files.map((file) => {console.log(file);return (file.mimetype.split('/')[0].includes('image') && (<divstyle={{padding: 16,border: '1px solid gray',borderRadius: 5,margin: '16px 0',}}key={file.filename}><imgsrc={'/' + file.path}/* Note the '/'. we added a slash prefix because our file pathcomes in this format: images/<filename>.jpg.*/ alt={file.filename}style={{ width: '100%' }}/><p>{file.filename}</p></div>));})}</>);}}
If you have files in your database then you should be able to see them in your browser.
Create a file and name it uploadWithPreview.js
in your src/components
directory then add the following piece of code
import React, { useEffect, useState } from 'react';import { useDropzone } from 'react-dropzone';import { useMutation } from '@apollo/react-hooks';import { UploadMutation } from './upload';import { FileQuery } from './Uploads'; // import FileQuery we created in the Uploads.js fileexport default function WithPreviews(props) {const [file, setFile] = useState({}); // empty state that will be populated with a file objectconst [uploadFile] = useMutation(UploadMutation);// submit functionconst handleUpload = async () => {if (file) {uploadFile({variables: { file },refetchQueries: [{ query: FileQuery, variables: file }], // update the store after a successful upload.});setFile({}); // reset state after a successful uploadconsole.log('Uploaded successfully: ', file);} else {console.log('No files to upload');}};const { getRootProps, getInputProps } = useDropzone({accept: 'image/*',onDrop: (acceptedFile) => {setFile(// convert preview string into a URLObject.assign(acceptedFile[0], {preview: URL.createObjectURL(acceptedFile[0]),}),);},});const thumbs = (<div className='thumb' key={file.name}><div className='thumb-inner'><img src={file.preview} className='img' alt={file.length && 'img'} /></div></div>);useEffect(() => () => {URL.revokeObjectURL(file.preview);},[file],);return (<section className='container'><div {...getRootProps({ className: 'dropzone' })}><input {...getInputProps()} /><p>Drag 'n' drop some file here, or click to select file</p></div><aside className='thumb-container'>{thumbs}<buttontype='submit'className={`button`}style={{ display: file && !Object.keys(file).length && 'none' }}onClick={handleUpload}>Upload</button></aside></section>);}
Congratulations if you made it to this pont 👏
Conclusion#
Handling file upload on both Rest and Graph APIs are a little bit tricky. However, with modern tools we can now upload files with lesser effort.
- We learnt how to setup a react application for uploads based on a graphql api.
- We also learnt how to configure our backend so that it can serve files to the client.
I hope you find this helpful.