Build a live photo feed using React and Cloudinary
You will need Node 6+ and npm installed on your machine. A basic knowledge of JavaScript (ES6) and React will be helpful.
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.
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:
// server.js
// import dependencies
require('dotenv').config({ path: 'variables.env' });
const express = require('express');
const multipart = require('connect-multiparty');
const bodyParser = require('body-parser');
const cloudinary = require('cloudinary');
const cors = require('cors');
const Datastore = require('nedb');
const Pusher = require('pusher');
// Create an express app
const app = express();
// Create a database
const db = new Datastore();
// Configure middlewares
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// Setup multiparty
const multipartMiddleware = multipart();
app.set('port', process.env.PORT || 5000);
const server = app.listen(app.get('port'), () => {
console.log(`Express running → PORT ${server.address().port}`);
});
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 intoprocess.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:
// variables.env
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
Head over to the Pusher website and sign up for a free account. Select Channels apps on the sidebar, and hit Create Channels app to create a new app. Once your app is created, retrieve your credentials from the API Keys tab, then add the following to your variables.env
file:
// variables.env
PUSHER_APP_ID=<your app id>
PUSHER_APP_KEY=<your app key>
PUSHER_APP_SECRET=<your app secret>
PUSHER_APP_CLUSTER=<your app cluster>
Next, initialize the Pusher SDK within server.js
:
// server.js
...
const db = new Datastore();
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: process.env.PUSHER_APP_CLUSTER,
encrypted: true,
});
...
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:
// variables.env
CLOUDINARY_CLOUD_NAME=<your cloud name>
CLOUDINARY_API_KEY=<your api key>
CLOUDINARY_API_SECRET=<your api secret>
Next, initialize the Cloudinary SDK within server.js
under the pusher
variable:
// server.js
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
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:
// server.js
app.get('/', (req, res) => {
db.find({}, (err, data) => {
if (err) return res.status(500).send(err);
res.json(data);
});
});
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.
// server.js
app.post('/upload', multipartMiddleware, (req, res) => {
// Upload image
cloudinary.v2.uploader.upload(req.files.image.path, {}, function(
error,
result
) {
if (error) {
return res.status(500).send(error);
}
// Save image to database
db.insert(Object.assign({}, result, req.body), (err, newDoc) => {
if (err) {
return res.status(500).send(err);
}
//
pusher.trigger('gallery', 'upload', {
image: newDoc,
});
res.status(200).json(newDoc);
});
});
});
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.
- 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:
// src/App.css
body {
font-family: 'Roboto', sans-serif;
}
.App {
margin-top: 40px;
}
.App-title {
text-align: center;
}
img {
max-width: 100%;
}
form {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
font-size: 18px;
}
.label {
display: block;
margin-bottom: 20px;
font-size: 20px;
}
input[type="file"] {
margin-bottom: 20px;
}
button {
border: 1px solid #353b6e;
border-radius: 4px;
color: #f7f7f7;
cursor: pointer;
font-size: 18px;
padding: 10px 20px;
background-color: rebeccapurple;
}
.loading-indicator {
display: flex;
justify-content: center;
margin-top: 30px;
}
.gallery {
display: grid;
grid-template-columns: repeat(3, 330px);
grid-template-rows: 320px 320px 320px;
grid-gap: 20px;
width: 100%;
max-width: 1000px;
margin: 0 auto;
padding-top: 40px;
}
.photo {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #d5d5d5;
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);
}
Application logic
Open up src/App.js
and change its contents to look like this:
// src/App.js
import React, { Component } from 'react';
import axios from 'axios';
import Pusher from 'pusher-js';
import Spinner from 'react-spinkit';
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {
images: [],
selectedFile: null,
loading: false,
};
}
componentDidMount() {
this.setState({
loading: true,
});
axios.get('http://localhost:5000').then(({ data }) => {
this.setState({
images: [...data, ...this.state.images],
loading: false,
});
});
const pusher = new Pusher('<your app key>', {
cluster: '<your app cluster>',
encrypted: true,
});
const channel = pusher.subscribe('gallery');
channel.bind('upload', data => {
this.setState({
images: [data.image, ...this.state.images],
});
});
}
fileChangedHandler = event => {
const file = event.target.files[0];
this.setState({ selectedFile: file });
};
uploadImage = event => {
event.preventDefault();
if (!this.state.selectedFile) return;
this.setState({
loading: true,
});
const formData = new FormData();
formData.append(
'image',
this.state.selectedFile,
this.state.selectedFile.name
);
axios.post('http://localhost:5000/upload', formData).then(({ data }) => {
this.setState({
loading: false,
});
});
};
render() {
const image = (url, index) => (
<img alt="" className="photo" key={`image-${index} }`} src={url} />
);
const images = this.state.images.map((e, i) => image(e.secure_url, i));
return (
<div className="App">
<h1 className="App-title">Live Photo Feed</h1>
<form method="post" onSubmit={this.uploadImage}>
<label className="label" htmlFor="gallery-image">
Choose an image to upload
</label>
<input
type="file"
onChange={this.fileChangedHandler}
id="gallery-image"
accept=".jpg, .jpeg, .png"
/>
<button type="submit">Upload!</button>
</form>
<div className="loading-indicator">
{this.state.loading ? <Spinner name="spinner" /> : ''}
</div>
<div className="gallery">{images}</div>
</div>
);
}
}
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 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:
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.
Thanks for reading! Remember that you can find the source code of this app in this GitHub repository.
5 September 2018
by Neo Ighodaro