React Native File Upload using GraphQL & Apollo
Tags: reactnative, graphql, apollo, react
Published: 2020-09-03 23:32:40

---
title: React Native File Upload using GraphQL & Apollo
published: true
date: 2020-09-03 00:00:00 UTC
tags: reactnative, graphql, apollo, react
canonical_url: https://www.delvalle.dev/posts/react-native-file-upload-graphql-digitalocean/react-native-file-upload-graphql-digitalocean/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/mr5qk0bj4hxykvdk2pr8.png
---

It's been a couple of months since my last article. I've been pretty busy working on [KnobsAI](https://knobs.ai) and haven't had much time to write.

I thought it would be nice to share how I implemented the file upload feature in KnobsAI so here's a short article on that.

Today I'll be showing how to upload pictures from a React Native app to Digital Ocean Storage, using GraphQL and Apollo.

While the example is pretty simple, it sets the ground for more complex stuff. The pictures will be uploaded to Digital Ocean Storage, which uses the AWS API, but you can apply the same logic to upload them to a different service.

If you are using Digital Ocean Storage, you'll need to:

- [Create a DigitalOcean Space & API Key](https://www.digitalocean.com/community/tutorials/how-to-create-a-digitalocean-space-and-api-key)
- [Add the Access Keys to the AWS Credentials file](https://www.digitalocean.com/community/tutorials/how-to-upload-a-file-to-object-storage-with-node-js#add-access-keys-to-credentials-file)

I used the guide from the second link as a starting point for this feature. It doesn't use GraphQL tho, which is what I introduced in my project and today's guide.

Here's the [repo](https://github.com/dlvx/rn-file-upload-graphql) with the source code in case you want to fork it.

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#server-side-architecture)Server-Side Architecture

The server side is comprised of three files: the index, the schema, and the storage.

In the index.js file, we define our ApolloServer and the Express app. If you've already worked with GraphQL you may have done this differently as there are many ways to do it. The important thing here is the Storage service that is being passed in the ApolloServer context so every resolver can make use of it.

```js
const express = require('express');
const Storage = require('./storage');
const { ApolloServer } = require('apollo-server-express');
const { typeDefs, resolvers } = require('./schema');

const PORT = process.env.SERVER_PORT || 4000;

const app = express();

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({
    req,
    res,
  }) => ({
    req,
    res,
    Storage
  }),
  playground: {
    endpoint: `http://localhost:${PORT}/graphql`
  },
});

server.applyMiddleware({
  app
});

app.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
})
```

The schema es where we define our mutation resolver that will receive the image object from the React Native app and pass it to the Storage service. As you can see, the Storage service is available via the context parameter because we injected it when setting up the server.

```js
const {
  gql,
  GraphQLUpload
} = require('apollo-server-express');

const uploadSchema = gql`
  type Query {
    _empty: String
  }
  type Mutation {
    uploadImage(
      image: Upload
    ): String
  }
`

module.exports = {
  typeDefs: [uploadSchema],
  resolvers: {
    Upload: GraphQLUpload,
    Mutation: {
      uploadImage: async (root, { image }, {
        Storage
      }) => {
        const folder = `rn-upload/`;
        try {
          const uploadResult = await Storage.upload(image, folder);
          return uploadResult.uri;
        } catch(e) {
          return new Error(e);
        }
      },
    }
  }
};
```

The Storage service is responsible for communicating with the Digital Ocean Storage via the AWS API. Remember from the guide above, you need to store the access keys to your bucket in a `.aws/credentials` file.

An important thing to note here. The image property received in the resolver above is being sent using `apollo-upload-client` and it's an object containing a filename, a mime-type, the encoding, and a Read Stream.

The Read Stream is what we need to pass to the `s3.upload` function as the `Body`. It took me some time to figure this out as I was passing the entire file object

```js
const aws = require('aws-sdk');
const { v4: uuid } = require('uuid');
const { extname } = require('path');

// Set S3 endpoint to DigitalOcean Spaces
const spacesEndpoint = new aws.Endpoint('nyc3.digitaloceanspaces.com');
const s3 = new aws.S3({
  endpoint: spacesEndpoint,
  params: {
    ACL: 'public-read',
    Bucket: 'your-bucket-name',
  },
});

async function upload(file, folder){

  if(!file) return null;

  const { createReadStream, filename, mimetype, encoding } = await file;

  try {
    const { Location } = await s3.upload({ 
      Body: createReadStream(),               
      Key: `${folder}${uuid()}${extname(filename)}`,  
      ContentType: mimetype                   
    }).promise();         
        
    return {
      filename,
      mimetype,
      encoding,
      uri: Location, 
    }; 
  } catch(e) {
    return { error: { msg: 'Error uploading file' }};
  }
}

module.exports = {
  upload,
};
```

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#client-side-architecture)Client-Side Architecture

As for the React Native side, the important thing here is integrating `apollo-upload-client` into the mix.We need to pass an upload link to our ApolloClient using `createUploadLink`.

Also, don't forget to put your computer's IP if you're running the app on a simulator/emulator, or whatever IP you're using to run the server app.

```js
import React from 'react';
import { ApolloClient } from '@apollo/client';
import { InMemoryCache } from 'apollo-boost';
import { createUploadLink } from 'apollo-upload-client';
import { ApolloProvider } from '@apollo/react-hooks';
import ImageUploader from './ImageUploader';

// Use your computer's IP address if you're running the app in a simulator/emulator
// Or the IP address of the server you're running the node backend
const IP = '0.0.0.0'
const uri = `http://${IP}:4000/graphql`;

const client = new ApolloClient({
  link: createUploadLink({ uri }),
  cache: new InMemoryCache(),
});

export default function App() {

  return (
    <ApolloProvider client={client}>
      <ImageUploader />
    </ApolloProvider>
  );
}
```

If you happen to have several links you'd need to use `ApolloLink.from` as in the following example:

```js
const client = new ApolloClient({
  link: ApolloLink.from([
    errorLink,
    requestLink,
    createUploadLink({ uri }),
  ]),
  cache: new InMemoryCache(),
});
```

We then have an ImageUploader component, which uses an ImagePicker to let you choose an image from the phone's gallery and then calls the `uploadImage` mutation. The important thing here is to use the `ReactNativeFile` constructor from the `apollo-upload-client` package which will generate the object with the Read Stream we discussed above.

Everything else is pretty much UI stuff like showing a loading spinner while the image is being uploaded and a status message when it fails or succeeds. If it succeeds it will display the URL where the image was uploaded.

```js
import React, { useState, useEffect } from 'react';
import { StyleSheet, Button, View, Image, Text, ActivityIndicator } from 'react-native';
import Constants from 'expo-constants';
import * as ImagePicker from 'expo-image-picker';
import { gql } from 'apollo-boost';
import { useMutation } from '@apollo/react-hooks';
import { ReactNativeFile } from 'apollo-upload-client';
import * as mime from 'react-native-mime-types';

function generateRNFile(uri, name) {
  return uri ? new ReactNativeFile({
    uri,
    type: mime.lookup(uri) || 'image',
    name,
  }) : null;
}

const UPLOAD_IMAGE = gql`
  mutation uploadImage($image: Upload) {
    uploadImage(image: $image)
  }
`;

export default function App() {

  const [image, setImage] = useState(null);
  const [status, setStatus] = useState(null);
  const [uploadImage, { data, loading }] = useMutation(UPLOAD_IMAGE);

  useEffect(() => {
    (async () => {
      if (Constants.platform.ios) {
        const { status } = await ImagePicker.requestCameraRollPermissionsAsync();
        if (status !== 'granted') {
          alert('Sorry, we need camera roll permissions to make this work!');
        }
      }
    })();
  }, []);

  async function pickImage () {
    const result = await ImagePicker.launchImageLibraryAsync({
      allowsEditing: true,
      allowsMultipleSelection: false,
      aspect: [4, 3],
      quality: 1,
    });

    if (!result.cancelled) {
      setImage(result.uri);
    }
  };

  async function onUploadPress() {
    status && setStatus(null);
    const file = generateRNFile(image, `picture-${Date.now()}`);
    try {
      await uploadImage({
        variables: { image: file },
      });
      setStatus('Uploaded')
    } catch (e) {
      setStatus('Error')
    }
  }

  return (
    <View style={styles.container}>
      <Button title="Pick an image from camera roll" onPress={pickImage}/>
      {image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}
      {image && <Button title={ loading ? "Uploading" : "Upload"} onPress={onUploadPress} disabled={loading}/>}
      {
        loading && (
          <ActivityIndicator size="small" style={styles.loading}/>
        )
      }
      <Text style={{ color: status === 'Uploaded' ? 'green' : 'red'}}>{status}</Text>
      {
        status === 'Uploaded' && (
          <Text>URL: {data.uploadImage}</Text>
        )
      }
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  loading: {
    margin: 16,
  }
});
```

Now, this is a super simple example. You will most likely add more logic to this. Let's say, for example, a feature to let users change their profile picture. You'd need to wait for the Storage Service to give you the picture URL and then you'd modify that user in the database.

Here's how I'd do it:

```js
changeUserPicture: async ( 
  _,
  {
    _id,
    picture
  }, {
    User,
    Storage
  }
) => {

  const user = await User.findOne({ _id }); 

  if(user) {
    try {
      const folder = `users/${user._id}/profile/`;
      const { uri } = await Storage.upload(picture, folder);

      user.picture = uri;
      const updatedUser = await user.save(); 

      return updatedUser;
    } catch(e) {
      console.log(e);
    }
  }

  return user;
  
},
```

That's pretty much it for today's article! I hope this was useful to you. Feel free to provide any feedback you like or reach out if you need help.

Once again, here's the [repo](https://github.com/dlvx/rn-file-upload-graphql) with the source code in case you want to fork it.

Thanks for reading!