Whether it's for sharing your knowledge or giving the world a chance to live vicariously through your experiences, vlogs have provided a more immersive and engaging medium for sharing content and unleashing the creativity of content providers.
In this article, I will show you how to build a simple vlog using React. The application will have an index view displaying all the videos and another for uploading a new video.
We will take advantage of Cloudinary's comprehensive API and easy-to-use transformation URLs to manage media resources. Not only will we be able to store resources via Cloudinary, but uploaded videos can also be automatically transcoded to all relevant formats suitable for web viewing, optimized for web browsers and mobile devices, and transformed in real-time. This makes for a seamless user experience with minimal coding. You will need a Cloudinary account for this tutorial. Create one here if you don't already have one.
For UI components in our application, we will use antd while axios will be used for uploading our videos to Cloudinary.
Here's a link to the demo on CodeSandbox.
Getting started
Create a new React app using the following command:
1npx create-react-app cloudinary_vlog_demo
Next, add the project dependencies using the following command:
1npm install antd @ant-design/icons cloudinary-react axios react-router-dom@6
We need to import the antd CSS. To do this, open your src/App.css
file and the following to it:
1@import '~antd/dist/antd.css';
Next, we need to create some environment variables to hold our Cloudinary details. We will be sending images to Cloudinary via unsigned POST requests for this tutorial. To do this, we need our account cloud name and an unsigned upload preset. Create a new file called .env
at the root of the project and add the following to it:
1REACT_APP_CLOUD_NAME = 'INSERT YOUR CLOUD NAME HERE'2 REACT_APP_UPLOAD_PRESET = 'INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE'
This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env
file using the following command.
1cp .env .env.local
By default, this local file is added to .gitignore
and mitigates the security risk of inadvertently exposing secret credentials to the public. You can update your .env.local
file with your Cloudinary cloud name and generated upload preset.
Create a new folder named util
in the src' directory. This folder will hold all the helper classes we will need in our components. In the
utilfolder, create a file called
cloudinaryConfig.js. This file will give access to the environment variables and prevent repeated
process.env.calls throughout the project. Add the following to the
cloudinaryConfig.js` file:
1export const cloudName = process.env.REACT_APP_CLOUD_NAME;2 export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;3 export const uploadTag = "my_cloudinary_vlog";
In addition to accessing the environment variables, we also declare a constant named uploadTag
. This will be attached to the videos on upload and used to retrieve the vlog posts from Cloudinary.
Create Helper Class for API Requests
We need to make two API requests for this application: uploading a new video and retrieving the list of uploaded videos. Create a new file named api.js
in the util
folder and add the following to it:
1import axios from "axios";2 import {cloudName, uploadPreset, uploadTag} from "./cloudinaryConfig";34 export const getVideos = ({successCallback}) => {5 axios6 .get(`https://res.cloudinary.com/${cloudName}/video/list/${uploadTag}.json`)7 .then((response) => successCallback(response.data.resources));8 };910 const formatMetadata = metadata => {11 return Object.entries(metadata)12 .reduce(13 (result, [key, value]) => `${result}${result !== '' ? '|' : ''}${key}=${value}`14 , ''15 );16 }1718 export const uploadVideo = ({file, metadata, successCallback}) => {19 const url = `https://api.cloudinary.com/v1_1/${cloudName}/video/upload`;20 const data = new FormData();21 data.append("file", file);22 data.append("upload_preset", uploadPreset);23 data.append("context", formatMetadata(metadata));24 data.append("tags", uploadTag);25 axios26 .post(url, data, {27 headers: {28 "Content-Type": "multipart/form-data",29 },30 })31 .then(response => successCallback(response.data))32 }
In this file, we export two functions - getVideos
and uploadVideo
. The getVideos
function uses axios to make a GET
request to Cloudinary. The resources in the JSON response are passed as a parameter to the success callback function, which allows the component perform some action once the list of resources becomes available. This is made possible by Cloudinary’s Client-side asset lists feature.
To ensure that this feature is available on your Cloudinary account, you must ensure that the Resource list option is enabled. By default, the list delivery type is restricted. To enable it, open the Security settings in your Management console, and clear the Resource list item under Restricted image types. You may want to remove this option only temporarily, as needed. Alternatively, you can bypass this (and any) delivery type restriction using a signed URL.
The uploadVideo
function takes a file, some collected metadata on the video, and a callback function to be executed when the request is handled properly. Using the formatMetadata
function, the metadata
object is converted to the expected contextual metadata format for file uploads to Cloudinary. Essentially, an =
character separates a key from its value while a |
character separates key-value pairs. The file, upload preset, context metadata, and upload tag are passed in the formData request to Cloudinary. The provided success callback is used to trigger further action in the component once a successful response is received.
Create VideoUpload
Component
In the src
folder, create a folder called components
. In the src/components
folder, create a file called VideoUpload.js
and add the following code to it:
1import {useState} from "react";2 import {Button, Card, Col, Form, Input, message, Upload} from "antd";3 import {uploadVideo} from "../util/api";4 import {UploadOutlined} from "@ant-design/icons";5 import {Navigate} from "react-router-dom";67 const VideoUpload = () => {8 const [isUploading, setIsUploading] = useState(false);9 const [uploadedFile, setUploadedFile] = useState(null);10 const [shouldRedirect, setShouldRedirect] = useState(false);1112 const formItemLayout = {13 labelCol: {14 sm: {span: 4},15 },16 wrapperCol: {17 sm: {span: 18},18 },19 };2021 const onFinish = (values) => {22 if (uploadedFile === null) {23 message.error('You need to upload a video first');24 } else {25 setIsUploading(true);26 uploadVideo({27 file: uploadedFile,28 metadata: values,29 successCallback: () => {30 setIsUploading(false);31 setShouldRedirect(true);32 }33 });34 }35 };3637 const onFailedSubmission = (errorInfo) => {38 console.log("Failed:", errorInfo);39 };4041 const props = {42 name: "file",43 onRemove: () => {44 setUploadedFile(null);45 },46 beforeUpload: (file) => {47 setUploadedFile(file);48 return false;49 },50 showUploadList: false,51 maxCount: 1,52 };5354 return (shouldRedirect ? <Navigate to='/'/> :55 <Card style={{margin: "auto", width: "50%"}}>56 <Form57 {...formItemLayout}58 onFinish={onFinish}59 onFinishFailed={onFailedSubmission}60 autoComplete="off"61 >62 <Form.Item63 label="Title"64 name="title"65 rules={[{66 required: true,67 message: "Please provide a title for the video",68 }]}69 >70 <Input/>71 </Form.Item>72 <Form.Item73 name="description"74 label="Description"75 rules={[{76 required: true,77 message: 'Please provide a brief summary of the video'78 }]}79 >80 <Input.TextArea showCount maxLength={1000}/>81 </Form.Item>82 <Col span={8} offset={9} style={{marginBottom: '10px'}}>83 <Upload {...props}>84 <Button icon={<UploadOutlined/>} loading={isUploading}>85 Click to Upload Video86 </Button>87 </Upload>88 </Col>89 <Form.Item wrapperCol={{offset: 5, span: 16}}>90 <Button91 type="primary"92 htmlType="submit"93 loading={isUploading}94 >95 Submit96 </Button>97 </Form.Item>98 </Form>99 </Card>100 );101 };102103 export default VideoUpload;
This component renders the form that will be used to upload videos. In addition to the video file, we also make provision for the user to provide the title of the post and a brief description. For the sake of this tutorial, the post title and description are required.
The onFinish
function is called when the user clicks the Submit button after filling the required fields. The function checks to ensure that a file has been uploaded before making a POST request using the uploadVideo
we declared earlier. The function passed as a callback on success sets the value shouldRedirect
to true. This, in turn, triggers the rendering of the Navigate component provided by React Router, which redirects the user to the index page.
By returning false
in the beforeUpload
key of props, we disable the default behavior of the Upload component, which is to upload the selected file to a provided URL. We also restrict the maximum number of files to 1.
Create VideoGrid
Component
In the src/components
folder, create a file called VideoGrid.js
and add the following to it:
1import React, {useEffect, useState} from "react";2 import {getVideos} from "../util/api";3 import {Card, Col, Row} from "antd";4 import {Video} from 'cloudinary-react';5 import {cloudName} from "../util/cloudinaryConfig";67 const VideoGrid = () => {89 const [videos, setVideos] = useState([]);1011 useEffect(() => {12 getVideos({13 successCallback: setVideos14 })15 }, []);1617 return <>18 <h1>Cloudinary Powered Vlog</h1>19 <Row20 gutter={[16, 16]}21 justify='center'22 align='middle'23 >24 {videos.map(video => {25 const {title, description} = video.context.custom;26 const {public_id: publicId} = video;27 return <Col key={publicId}>28 <Card style={{width: '600px'}} cover={29 <Video30 cloudName={cloudName}31 publicId={publicId}32 controls={true}33 width='480'/>34 }>35 <Card.Meta title={title} description={description}/>36 </Card>37 </Col>38 })}39 </Row>40 </>;4142 };4344 export default VideoGrid;
On render, we use an effect to retrieve the list of videos using the getVideos
function we declared earlier. The returned resources are saved to the videos
state variable. Each video is rendered using the Video
component provided by cloudinary-react
with the title and description rendered underneath.
Creating the Menu
In the src/components
folder, create a file named Menu.js
and add the following to it:
1import {Menu as AntDMenu} from 'antd';2 import {useState} from "react";3 import {Link} from "react-router-dom";4 import {HomeOutlined, UploadOutlined} from "@ant-design/icons";56 const Menu = () => {7 const [currentlySelected, setCurrentlySelected] = useState('home');89 const handleMenuSelection = e => {10 setCurrentlySelected(e.key);11 }1213 return (14 <AntDMenu15 mode='horizontal'16 onClick={handleMenuSelection}17 selectedKeys={[currentlySelected]}18 >19 <AntDMenu.Item key='home' icon={<HomeOutlined/>}>20 <Link to='/'>Home</Link>21 </AntDMenu.Item>22 <AntDMenu.Item key='upload' icon={<UploadOutlined/>}>23 <Link to='/upload'>Upload Video</Link>24 </AntDMenu.Item>25 </AntDMenu>26 );2728 };2930 export default Menu;
Using the AntD Menu component, we provide a means of navigation between the home page (/) and the video upload page (/upload).
Routing Implementation
Now that we have all our components in place let’s implement the routing functionality to allow us navigate between components. In the src/util
folder, create a file called routes.js
and add the following to it:
1import VideoGrid from "../components/VideoGrid";2 import VideoUpload from "../components/VideoUpload";34 export default [5 {6 path: '/',7 element: <VideoGrid/>8 },9 {10 path: '/upload',11 element: <VideoUpload/>12 }13 ];
Here we define Route Objects, which will be used by the useRoutes
hook to render the <Route>
component.
Next, update src/App.js
to match the following code:
1import './App.css';2 import {useRoutes} from "react-router-dom";3 import routes from "./util/routes";4 import Menu from "./components/Menu";5 import {Col, Row} from "antd";67 function App() {89 const router = useRoutes(routes);1011 return <div style={{margin: "1%"}}>12 <Menu/>13 <div style={{textAlign: 'center'}}>14 <Row15 justify='center'16 align='middle'17 style={{textAlign: 'center'}}18 >19 <Col style={{width: '100%', margin: '2%'}}>20 {router}21 </Col>22 </Row>23 </div>24 </div>25 }2627 export default App;
Using the earlier declared routes
object and the useRoutes
hook, we render a <Route>
component.
Finally, wrap the <App>
component in a BrowserRouter
. To do this, update src/index.js
to match the following:
1import React from 'react';2 import ReactDOM from 'react-dom';3 import './index.css';4 import App from './App';5 import {BrowserRouter} from "react-router-dom";67 ReactDOM.render(8 <React.StrictMode>9 <BrowserRouter>10 <App/>11 </BrowserRouter>12 </React.StrictMode>,13 document.getElementById('root')14 );
With this in place, our application is complete! Run your application using this command:
1npm start
By default, the application will be available at http://localhost:3000/. The final result will look like the gif shown below.
Find the complete project here on GitHub.
Conclusion
In this article, we looked at how Cloudinary can be used to manage the media content for a vlog. One of the major advantages of using Cloudinary for delivering video content is that uploaded videos can be automatically transcoded to all relevant formats suitable for web viewing, optimized for web browsers and mobile devices, and transformed in real-time to fit your graphic design.
Resources you may find helpful: