React Selfie App with Cloudinary

Ifeoma Imoh

In this article, we'll learn how to build an application that allows us to take pictures using a webcam. In other words, we will build a selfie application using React.js and then use the rich image transformation features provided by Cloudinary to apply unique special effects to our captured images. This should be a lot of fun! Let's get started.

This article assumes you are familiar with the basics of JavaScript and React.

Here is a link to the demo on CodeSandbox and the full source code here on GitHub.

Setting up Cloudinary

We will be using Cloudinary to take advantage of the SDKs, so you need to create an account if you do not have one already.

Log in after creating your account, and on your dashboard page, you should see all your credentials (cloud name, etc.) Before we can apply transformations to an image, the image needs to be uploaded to Cloudinary servers. Since our uploads will be done from our client-side application, we need to define an upload preset. Upload presets can either be signed or unsigned and we would be using the unsigned presets.

If you don't have an unsigned preset already, you can create one as follows:

  • Go to your Cloudinary dashboard and click on the settings icon.
  • Select the Upload tab and scroll down to the Upload presets section.
  • Click the Add upload preset link.
  • In the form displayed, add a name for your upload preset and set the Signing mode to unsigned, and click the Save button.

Take note of the preset name because it will be used later to upload captured images.

Environment Setup

Open your terminal and run the following command to bootstrap a React app in a folder called selfie-app.

1npx create-react-app selfie-app

The next step is to install the dependencies that we will need in this project. Run the following command to install them.

1npm install --save @cloudinary/url-gen axios react-webcam

The command above installs the Cloudinary SDKs that we will use in our application. We will be using the @Cloudinary/url-gen package to configure Cloudinary, build image URLs and apply transformations (filters and visual enhancements). Axios will be our HTTP client for uploading images, and react-webcam will be used to access the user's camera and capture images.

A bird's-eye view of all the moving parts that make up our app is provided below. Include the following code in your App.js file.

1import "./App.css";
2 import { useRef, useState } from "react";
3 import Webcam from "react-webcam";
4 import axios from "axios";
5 import { Cloudinary } from "@cloudinary/url-gen";
6 import { Effect } from "@cloudinary/url-gen/actions/effect";
7
8 //Add your cloud name
9 let cloudName = "ADD-YOUR-CLOUD-NAME-HERE";
10 const cld = new Cloudinary({
11 cloud: {
12 cloudName,
13 },
14 });
15 function applyFilter(filter, image) {
16 // this will be used to apply filters
17 }
18 const filters = [
19 // strings representing a filter
20 ];
21 function ImagePreviewer() {
22 // this will be used to display our image
23 }
24 function FilterItem({ imgId, setPrevURL, filterName }) {
25 //this will be used to apply a filter
26 }
27 const App = () => {
28 // this will hold our camera stream, imagePreviewer and filter components
29 };
30 export default App;

First, we're importing react hooks, Axios, and some components from the Cloudinary SDK. We then create an instance of the Cloudinary class and initialized it with our cloud name. The Cloudinary instance will be used to build the delivery URLs for our assets.

Next, we define a function called applyFilter, which accepts a filter and an image to which the desired filter will be applied. We also define an array that will hold strings each representing the different filters (e.g., sepia, greyscale) that will be applied to our images.

Finally, we defined some components we'll need and exported our App component. Over the following sections, we will be visiting each function as the need arises to update their logic as we build our application.

Setting up Camera

In your App.js file, update your App component with the following:

1const App = () => {
2 const constraints = {
3 width: 700,
4 height: 550,
5 facingMode: "user",
6 aspectRatio: 9 / 16,
7 };
8 const camRef = useRef();
9 const [loading, setLoading] = useState(false);
10 return (
11 <section className="main">
12 <article className="media_box">
13 <Webcam
14 // this will be used internally to bind some useful methods to the ref variable.
15 ref={camRef}
16 videoConstraints={constraints}
17 //this specifies the file format we want for the captured
18 screenshotFormat="image/jpeg"
19 />
20 {/* this button will be used to capture the image*/}
21 <button
22 disabled={loading}
23 className="capture_btn"
24 ></button>
25 </article>
26 </section>
27 );
28 };
29 export default App;

In the code above, we created an object that defines the media constraints of the video recording before an image is captured. We also defined a reference that will give us access to our web camera using the useRef hook and a state variable to toggle between loading states when uploading an image.

Finally, we returned the Webcam component and a button that is disabled when loading.

If you open your browser on localhost:3000, should see this:

Under the hood, the Webcam component requests permission to access media devices as defined in the audio and video constraints. In this case, it requests access to the camera. When authorized, it receives a media stream object, creates a video element, and uses the media stream object as its source.

Capture, Upload, Delete and Display Features

At this point, we can't do much with what we have. Let's add some functionalities to be able to capture and upload an image.

Add the following to your App component.

1const App = () => {
2 const constraints = {
3 width: 700,
4 height: 550,
5 facingMode: "user",
6 aspectRatio: 9 / 16,
7 };
8 const camRef = useRef();
9 const [loading, setLoading] = useState(false);
10 const [id, setId] = useState("");
11 const [prevURL, setPrevURL] = useState("");
12 const captureAndUpload = async () => {
13 // get screenshot
14 const data = camRef.current.getScreenshot();
15 // upload to cloudinary and get public_id
16 try {
17 setLoading(true);
18 const imageData = new FormData();
19 imageData.append("file", data);
20
21 // Add your upload preset here
22 imageData.append("upload_preset", "ADD-YOUR-UPLOAD-PRESET-HERE");
23 const res = await axios.post(
24 ` https://api.cloudinary.com/v1_1/${cloudName}/image/upload`,
25 imageData
26 );
27 const imageDetails = res.data;
28 setId(imageDetails.public_id);
29 setPrevURL(imageDetails.url);
30 } catch (error) {
31 console.log(error);
32 } finally {
33 setLoading(false);
34 }
35 //set publicID
36 };
37 const deleteImage = () => {
38 setPrevURL("");
39 setId("");
40 };
41 return (
42 <section className="main">
43 <article className="media_box">
44 <Webcam
45 ref={camRef}
46 videoConstraints={constraints}
47 screenshotFormat="image/jpeg"
48 />
49 {/* this button will be used to capture the image*/}
50 <button
51 disabled={loading}
52 onClick={captureAndUpload}
53 className="capture_btn"
54 ></button>
55 </article>
56 </section>
57 );
58 };

In the code above, we added two state variables called id and previewUrl. They will hold the public_id and the delivery URL, respectively. Before applying transformations to our media assets, we need to upload them to our Cloudinary account first. Each uploaded asset will be given a unique public ID that will be used to reference it and build its delivery URL.

We also defined CaptureAndUpload and deleteImage, which updates and clears our ID and previewURL variables. As the name implies, the function CaptureAndUpload gets the image data from the call to the getScreenShot function and saves it in a variable. We then toggle our loading state to prevent a user from clicking the button to capture another image during the upload process.

Before uploading, we construct our request body using a FormData object and specify key-value pairs that signify the following:

  • File: this is the file we want to upload. Cloudinary supports different file source options, and in this case, we are using the base64 encoded string representation of our image from the call to the getScreenshot function.
  • upload_preset: this string represents the unsigned upload preset we created earlier.

Since we are doing unsigned uploads, the file and upload_preset parameters are mandatory to see a complete list of other options; check here.

Next, we hit the Cloudinary upload endpoint and pass our request body as an input. If everything goes fine, we get an upload response object which contains information about the uploaded asset. We then take the public_ID and URL and store them in our id and previewURL variables. If we get an error, we print it to the console.

We created a deleteImage function that clears the content of our state variables in our React application, but the image will still be present on our Cloudinary account.

Now we can take pictures and upload them to Cloudinary, but we also need to display the image. In our App.js file, let's update our ImagePreviewer component.

1function ImagePreviewer({ url, deleteImage }) {
2 return url ? (
3 <div className="img_box">
4 <img src={url} alt="my_image" />
5 <button className="close_btn" onClick={deleteImage}>
6 Delete
7 </button>
8 </div>
9 ) : null;
10 }

This component accepts a URL and a deleteImage function. It renders an image and a button that triggers the deleteImage function when clicked.

Let's now update our app component to include the imagePreviewer component and pass in our id and previewURL as props.

1const App = () => {
2 // truncated for brevity
3 return (
4 <section className="main">
5 // truncated for brevity
6 {/* this button will be used to capture the image*/}
7 <button
8 disabled={loading}
9 onClick={captureAndUpload}
10 className="capture_btn"
11 ></button>
12 <ImagePreviewer url={prevURL} deleteImage={deleteImage} />
13 </article>
14 </section>
15 );
16
17 }

Fun with Filters

As mentioned in the introduction, Cloudinary's transformation features provide us with so many ways to manipulate media assets, including applying visual enhancements based on our application's needs. We will be using only a few filters but to see all available filters check here.

Let's populate our array with different filter options. Add the following to your filters array in your App.js file.

1const filters = [
2 "none",
3 "artistic",
4 "sepia",
5 "cartoonify",
6 "vignette",
7 "oilpaint",
8 "grayscale",
9 "vectorize",
10 "pixelate",
11 ];

Next, we update our applyFilter function to include the logic for applying different filters.

1function applyFilter(filter, image) {
2 switch (filter) {
3 case "artistic":
4 return image.effect(Effect.artisticFilter("fes"));
5 case "sepia":
6 return image.effect(Effect.sepia());
7 case "cartoonify":
8 return image.effect(Effect.cartoonify());
9 case "vignette":
10 return image.effect(Effect.vignette());
11 case "oilpaint":
12 return image.effect(Effect.oilPaint());
13 case "grayscale":
14 return image.effect(Effect.grayscale());
15 case "vectorize":
16 return image.effect(Effect.vectorize());
17 case "pixelate":
18 return image.effect(Effect.pixelate());
19 default:
20 return image;
21 }
22 }

The function applyFilter accepts a string and an image object. It uses a switch statement based on the filter object to modify and return the image object after applying the desired filter. We're using the effect action group and passing the appropriate action from the Effect factory method we imported.

Now we have our filters array and applyFilter function, let's update our FilterItem component. Add the following to your FilterItem component in your App.js file.

1function FilterItem({ imgId, setPrevURL, filterName }) {
2 let image = cld.image(imgId);
3 image = applyFilter(filterName, image);
4 const imgURL = image.toURL();
5 return (
6 <div className="filter_item" onClick={() => setPrevURL(imgURL)}>
7 <img src={imgURL} alt="" />
8 <span className="filter_des">{filterName}</span>
9 </div>
10 );
11 }

This function creates an image object using the id prop, and our Cloudinary instance then applies the desired filter. It also returns some JSX that includes a div that responds to click events and passes the URL representation of the transformed image object to modify the prevURL variable. It then displays the image and the filter name.

We can now update the JSX returned from our App Component.

1const App = () => {
2 // truncated for brevity
3 return (
4 <section className="main">
5 <article className="media_box">
6 // truncated for brevity
7 </article>
8 <article className="filter_container">
9 {id && (
10 <>
11 {filters.map((filter, index) => (
12 <FilterItem
13 imgId={id}
14 filterName={filter}
15 setPrevURL={setPrevURL}
16 key={index}
17 />
18 ))}
19 </>
20 )}
21 </article>
22 </section>
23 );
24 };

In the return statement of our App component, we first checked for the URL of our uploaded image. If it exists, we iterate over our filter strings and render our FilterItem component with the expected props. If we run our app now, we should be able to take pictures using our webcam and apply any filter of our choice.

Me, mySelfie and I btw your selfie is probably way better than mine. 🙂

Conclusion

In this article, we were able to build an application that allows us to take pictures using a webcam component. We also used the image transformation features provided by Cloudinary to apply unique special effects to our captured images.

Some resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.