File uploads with React and apollo (Part 2).


File uploads with React and apollo (Part 2).

Divine Hycenth12 min read

Published on March 06, 2021.

Last edited: March 06, 2021 by: Divine Hycenth


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#

Here is a demo of what we are going to build.

Complete example

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
# or
yarn create react-app react-apollo-upload
# Change directory into react-apollo-upload by running
cd 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 here
console.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;
Log files

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) {
path
id
filename
mimetype
}
}
`;
...

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-hooks
import { 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 files
const file = acceptedFiles[0];
// use the uploadFile variable created earlier
uploadFile({
// use the variables option so that you can pass in the file we got above
variables: { 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) {
path
id
filename
mimetype
}
}
`;
// 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 files
const file = acceptedFiles[0];
// use the uploadFile variable created earlier
uploadFile({
// use the variables option so that you can pass in the file we got above
variables: { 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;
Upload example

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#

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:

cors meme

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 cors
import path from 'path';
const app = express();
// Import your database configuration
import 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 directory
app.use(cors('*')); // All Cross-origin resource sharing from any network
server.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 hook
import gql from 'graphql-tag';
// FilesQuery
export const FileQuery = gql`
{
files {
id
filename
mimetype
path
}
}
`;
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 loaded
return <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') && (
<div
style={{
padding: 16,
border: '1px solid gray',
borderRadius: 5,
margin: '16px 0',
}}
key={file.filename}
>
<img
src={'/' + file.path}
/* Note the '/'. we added a slash prefix because our file path
comes 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 file
export default function WithPreviews(props) {
const [file, setFile] = useState({}); // empty state that will be populated with a file object
const [uploadFile] = useMutation(UploadMutation);
// submit function
const 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 upload
console.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 URL
Object.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}
<button
type='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 👏

Complete example

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.

Happy codding 💻 🙂#