Build a live photo feed using React and Cloudinary

Introduction

In this tutorial, we’ll go through how to build a photo feed with React and Cloudinary, while providing realtime updates to the feed using Pusher Channels. You can find the entire source code of the application in this GitHub repository.

Prerequisites

To follow along, a basic knowledge of JavaScript (ES6) and React is required. You also need to have the following installed on your machine:

Set up the server

Let’s set up a simple Node server for the purpose of uploading images to Cloudinary and triggering realtime updates with Pusher Channels.

The first step is to create a new empty directory and run npm init -y from within it. Next, install all the dependencies that we need for this project by running the command below:

    npm install express nedb cors body-parser connect-multiparty pusher cloudinary dotenv

Wait for the installation to complete, then create a file named server.js in the root of your project directory and populate the file with the following contents:

1// server.js
2    
3    // import dependencies
4    require('dotenv').config({ path: 'variables.env' });
5    const express = require('express');
6    const multipart = require('connect-multiparty');
7    const bodyParser = require('body-parser');
8    const cloudinary = require('cloudinary');
9    const cors = require('cors');
10    const Datastore = require('nedb');
11    const Pusher = require('pusher');
12    
13    // Create an express app
14    const app = express();
15    // Create a database
16    const db = new Datastore();
17    
18    // Configure middlewares
19    app.use(cors());
20    app.use(bodyParser.urlencoded({ extended: false }));
21    app.use(bodyParser.json());
22    
23    // Setup multiparty
24    const multipartMiddleware = multipart();
25    
26    app.set('port', process.env.PORT || 5000);
27    const server = app.listen(app.get('port'), () => {
28      console.log(`Express running → PORT ${server.address().port}`);
29    });

Here, we’ve imported the dependencies into our entry file. Here’s an explanation of what they all do:

  • express: A minimal and flexible Node.js server.
  • nedb: In memory database for Node.js.
  • connect-multiparty: Express middleware for parsing uploaded files.
  • body-parser: Express middleware for parsing incoming request bodies.
  • dotenv: Loads environmental variables from .env file into process.env.
  • pusher: Server SDK for Pusher Channels.
  • cloudinary: Cloudinary server SDK.

Create a variables.env file in the root of your project and add a PORT variable therein:

1// variables.env
2    
3    PORT:5000

Hard-coding credentials in your code is a bad practice so we’ve set up dotenv to load the app’s credentials from variables.env and make them available on process.env.

Set up Pusher Channels

To get started with Pusher Channels, sign up) for a free Pusher account. Then go to the dashboard and create a new Channels app.

Once your app is created, retrieve your credentials from the API Keys tab, then add the following to your variables.env file:

1// variables.env
2    
3    PUSHER_APP_ID=<your app id>
4    PUSHER_APP_KEY=<your app key>
5    PUSHER_APP_SECRET=<your app secret>
6    PUSHER_APP_CLUSTER=<your app cluster>

Next, initialize the Pusher SDK within server.js:

1// server.js
2    ...
3    const db = new Datastore();
4    
5    const pusher = new Pusher({
6      appId: process.env.PUSHER_APP_ID,
7      key: process.env.PUSHER_APP_KEY,
8      secret: process.env.PUSHER_APP_SECRET,
9      cluster: process.env.PUSHER_APP_CLUSTER,
10      encrypted: true,
11    });
12    
13    ...

Set up Cloudinary

Visit the Cloudinary website and sign up for a free account. Once your account is confirmed, retrieve your credentials from the dashboard, then add the following to your variables.env file:

react-cloudinary-credentials
1// variables.env
2    
3    CLOUDINARY_CLOUD_NAME=<your cloud name>
4    CLOUDINARY_API_KEY=<your api key>
5    CLOUDINARY_API_SECRET=<your api secret>

Next, initialize the Cloudinary SDK within server.js under the pusher variable:

1// server.js
2    
3    cloudinary.config({
4      cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
5      api_key: process.env.CLOUDINARY_API_KEY,
6      api_secret: process.env.CLOUDINARY_API_SECRET,
7    });

Create routes

We are going to create two routes for our application: the first one will serve all gallery images, while the second one handles the addition of a new image to the database.

Here’s the one that handles sending all images to the client. Add this above the port variable:

1// server.js
2    
3    app.get('/', (req, res) => {
4      db.find({}, (err, data) => {
5        if (err) return res.status(500).send(err);
6        res.json(data);
7      });
8    });

When this endpoint is hit, a JSON representation of all images that exist in the database will be sent to the client, except if an error is encountered, in which case a 500 server error will be sent instead.

Next, let’s add the route that adds new images sent from the client to the database.

1// server.js
2    
3    app.post('/upload', multipartMiddleware, (req, res) => {
4      // Upload image
5      cloudinary.v2.uploader.upload(req.files.image.path, {}, function(
6        error,
7        result
8      ) {
9        if (error) {
10          return res.status(500).send(error);
11        }
12        // Save image to database
13        db.insert(Object.assign({}, result, req.body), (err, newDoc) => {
14          if (err) {
15            return res.status(500).send(err);
16          }
17          //
18          pusher.trigger('gallery', 'upload', {
19            image: newDoc,
20          });
21          res.status(200).json(newDoc);
22        });
23      });
24    });

Here, the image is uploaded to Cloudinary and, on successful upload, a database entry is created for the image and a new upload event is emitted for the gallery channel along with the payload of the newly created item.

The code for the server is now complete. You can start it by running node server.js in your terminal.

Set up React app

Let's bootstrap our project using the create-react-app which allows us to quickly get a React application up and running. Open a new terminal window, and run the following command to install create-react-app on your machine:

    npm install -g create-react-app

Once the installation process is done, you can run the command below to setup your react application:

    create-react-app client

This command will create a new folder called client in the root of your project directory, and install all the dependencies needed to build and run the React application.

Next, cd into the newly created directory and install the other dependencies which we’ll be needing for our app’s frontend:

    npm install pusher-js axios react-spinkit
  • pusher-js: Client SDK for Pusher Channels.
  • axios: Promise based HTTP client for the browser and Node.
  • react-spinkit: Loading indicator component.

Finally, start the development server by running yarn start from within the root of the client directory.

Add the styles for the app

Within the client directory, locate src/App.css and change its contents to look like this:

1// src/App.css
2    
3    body {
4      font-family: 'Roboto', sans-serif;
5    }
6    
7    .App {
8      margin-top: 40px;
9    }
10    
11    .App-title {
12      text-align: center;
13    }
14    
15    img {
16      max-width: 100%;
17    }
18    
19    form {
20      text-align: center;
21      display: flex;
22      flex-direction: column;
23      align-items: center;
24      font-size: 18px;
25    }
26    
27    .label {
28      display: block;
29      margin-bottom: 20px;
30      font-size: 20px;
31    }
32    
33    input[type="file"] {
34      margin-bottom: 20px;
35    }
36    
37    button {
38      border: 1px solid #353b6e;
39      border-radius: 4px;
40      color: #f7f7f7;
41      cursor: pointer;
42      font-size: 18px;
43      padding: 10px 20px;
44      background-color: rebeccapurple;
45    }
46    
47    .loading-indicator {
48      display: flex;
49      justify-content: center;
50      margin-top: 30px;
51    }
52    
53    .gallery {
54      display: grid;
55      grid-template-columns: repeat(3, 330px);
56      grid-template-rows: 320px 320px 320px;
57      grid-gap: 20px;
58      width: 100%;
59      max-width: 1000px;
60      margin: 0 auto;
61      padding-top: 40px;
62    }
63    
64    .photo {
65      width: 100%;
66      height: 100%;
67      object-fit: cover;
68      background-color: #d5d5d5;
69      box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
70    }

Application logic

Open up src/App.js and change its contents to look like this:

1// src/App.js
2    
3    import React, { Component } from 'react';
4    import axios from 'axios';
5    import Pusher from 'pusher-js';
6    import Spinner from 'react-spinkit';
7    import './App.css';
8    
9    class App extends Component {
10      constructor() {
11        super();
12        this.state = {
13          images: [],
14          selectedFile: null,
15          loading: false,
16        };
17      }
18    
19      componentDidMount() {
20        this.setState({
21          loading: true,
22        });
23    
24        axios.get('http://localhost:5000').then(({ data }) => {
25          this.setState({
26            images: [...data, ...this.state.images],
27            loading: false,
28          });
29        });
30    
31        const pusher = new Pusher('<your app key>', {
32          cluster: '<your app cluster>',
33          encrypted: true,
34        });
35    
36        const channel = pusher.subscribe('gallery');
37        channel.bind('upload', data => {
38          this.setState({
39            images: [data.image, ...this.state.images],
40          });
41        });
42      }
43    
44      fileChangedHandler = event => {
45        const file = event.target.files[0];
46        this.setState({ selectedFile: file });
47      };
48    
49      uploadImage = event => {
50        event.preventDefault();
51    
52        if (!this.state.selectedFile) return;
53    
54        this.setState({
55          loading: true,
56        });
57    
58        const formData = new FormData();
59        formData.append(
60          'image',
61          this.state.selectedFile,
62          this.state.selectedFile.name
63        );
64    
65        axios.post('http://localhost:5000/upload', formData).then(({ data }) => {
66          this.setState({
67            loading: false,
68          });
69        });
70      };
71    
72      render() {
73        const image = (url, index) => (
74          <img alt="" className="photo" key={`image-${index} }`} src={url} />
75        );
76    
77        const images = this.state.images.map((e, i) => image(e.secure_url, i));
78    
79        return (
80          <div className="App">
81            <h1 className="App-title">Live Photo Feed</h1>
82    
83            <form method="post" onSubmit={this.uploadImage}>
84              <label className="label" htmlFor="gallery-image">
85                Choose an image to upload
86              </label>
87              <input
88                type="file"
89                onChange={this.fileChangedHandler}
90                id="gallery-image"
91                accept=".jpg, .jpeg, .png"
92              />
93              <button type="submit">Upload!</button>
94            </form>
95    
96            <div className="loading-indicator">
97              {this.state.loading ? <Spinner name="spinner" /> : ''}
98            </div>
99    
100            <div className="gallery">{images}</div>
101          </div>
102        );
103      }
104    }
105    
106    export default App;

I know that’s a lot of code to process in one go, so let me break it down a bit.

The state of our application is initialized with three values: images is an array that will contain all images in our photo feed, while selectedFile represents the currently selected file in the file input. loading is a Boolean property that acts as a flag to indicate whether the loading component, Spinner, should be rendered on the page or not.

When the user selects a new image, the fileChangedHandler() function is invoked, which causes selectedFile to point to the selected image. The Upload button triggers a form submission, causing uploadImage() to run. This function basically sends the image to the server and through an axios post request.

In the componetDidMount() lifecycle method, we try to fetch all the images that exist in the database (if any) so that on page refresh, the feed is populated with existing images.

The Pusher Channels client library provides a handy bind function that allows us to latch on to events emitted by the server so that we can update the application state. You need to update the pusher variable with your app key and cluster before running the code. Here, we’re listening for the upload event on the gallery channel. Once the upload event is triggered, our application is updated with the new image as shown below:

react-cloudinary-demo

Conclusion

You have now learned how easy it is to create a live feed and update several clients with incoming updates in realtime with Pusher Channels.

Thanks for reading! Remember that you can find the source code of this app in this GitHub repository.