Getting Started with React: Creating Your Own Blog App - Part 4
Introduction
In this article of our React blog series, we will learn how to edit, update and delete blogs from our blog app. Additionally, we will learn how to redirect to another page using a dynamic URL based on dynamic values. This blog post will deepen your understanding of React and simplify the learning process.
A quick note before we begin: We are building a blog app, and this blog series focuses on learning React through hands-on projects. This will be a basic app without any high-level concepts in React, but in the future, I will introduce some advanced concepts to enhance your understanding.
If you are new to React, I recommend reading my previous blogposts, in which I explain and demonstrate how to install React using Vite, as well as other project setup steps, and how to add and display a list of blogs. I strongly encourage you to read those blog posts before proceeding with this one.
Prerequisites
Familiar with HTML & CSS
Proficiency in Vanilla JavaScript
Any Code Editor
Nodejs Installed on the system
Read my previous blog posts to understand where we are in this series.
What will we learn in this blog?
In our previous blog, we learned how to add a blog and display a list of added blogs in our app. We covered two fundamental CRUD operations: Create and Read. In this blog, we will cover the remaining CRUD operations: Update and Delete.
In this blog, we will also learn how to navigate to other pages using dynamic values and dynamic URLs with React Router DOM. Working with dynamic routes and changing pages programmatically is something we will use in every app, such as editing a post based on its post ID, among other examples and also learn about useEffect
I intentionally added a few bugs in this code. I encourage you to find those bugs, raise an issue in my GitHub repo, and create a Pull Request to resolve them.
What is useEffect
in React?
The useEffect
hook in React is utilized for performing side effects in functional components. Side effects are actions that can impact other components, the DOM, or carry out other asynchronous operations. useEffect aids in managing these side effects by enabling you to execute code after the component has been rendered and updated. Here's a simple example to explain how useEffect
works:
import React, { useState, useEffect } from 'react';
function Counter() {
// State to hold the counter value
const [count, setCount] = useState(0);
// useEffect to update the document title
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // The effect runs whenever 'count' changes
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
Import the necessary React hooks:
useState
anduseEffect
.Define the
Counter
functional component that displays the current counter value and a button to increment it.Inside the component, use the
useState
hook to manage the counter value.Use the
useEffect
hook to set up a side effect. In this case, it updates the document title whenever thecount
value changes.The second argument of
useEffect
is an array of dependencies. If any of these dependencies change between renders, the effect will run again. In our case, we want the effect to run whenever thecount
value changes, so we provide[count]
as the dependency array.When the button is clicked to increment the counter, the
count
value changes. This triggers theuseEffect
to update the document title.
In simple terms, useEffect
enables you to execute actions after the component renders. By specifying dependencies, you can control when the effect should run.
Now, let's write some code that will edit, update and delete our blog app
Implement the editing, updating, and deleting features for the Blog app.
blog.jsx
// Import necessary components and libraries
import { Row, Container, Form } from "react-bootstrap";
import { useState, useEffect } from "react";
import { convertToRaw, EditorState, ContentState, convertFromHTML } from "draft-js";
import { Editor } from "react-draft-wysiwyg";
import draftToHtml from "draftjs-to-html";
import PropTypes from "prop-types";
import { useSearchParams, useNavigate } from "react-router-dom";
// Import local utility and component files
import CustomInputText from "../components/customInputText";
import CustomButton from "../components/customButton";
import CustomAlert from "../components/customAlert";
import formatDateTime from "../utils/format_date";
import isEditorEmpty from "../utils/editor_empty";
import "../css/button.css";
import "../css/blog.css";
// Create a functional component named "Blog"
function Blog({ onPassDatatoAppComponent, blogList, onUpdateDatatoAppComponent, onDeleteBlogFromBLogList }) {
// UseState Hooks to manage various states
const [blogTitle, setBlogTitle] = useState("");
const [blogContent, setBlogContent] = useState(EditorState.createEmpty());
const [formSubmitted, setFormSubmitted] = useState(false);
const [alert, setAlert] = useState(false);
const currentDate = new Date();
// console.log("Blog Component ",blogList);
//Route hooks
const [searchParams] = useSearchParams();
const navigate = useNavigate();
//UseEffect
useEffect(() => {
const blogId = searchParams.get("blogID");
if(blogList.length == 0){
navigate({pathname:'/blog'});
}
if(blogId && blogList.length != 0){
setBlogTitle(blogList[blogId].title);
setBlogContent(() => {
const blocksFromHTML = convertFromHTML(blogList[blogId].content)
const contentState = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap
)
return EditorState.createWithContent(contentState)
});
// console.log(editorState);
}
}, [blogList]);
// Function triggered when the "Publish Blog" button is clicked
const onSubmitBtnClick = (e) => {
e.preventDefault(); // Prevent the default form submission behavior
// Handling form submission
if (blogTitle && !isEditorEmpty(blogContent)) {
// If both title and content are provided
// Call the prop function to pass data
onPassDatatoAppComponent({
title: blogTitle,
content: draftToHtml(convertToRaw(blogContent.getCurrentContent())),
date: formatDateTime(currentDate),
});
// Reset form submission status
setFormSubmitted(false);
// Show success alert
setAlert(true);
// Clear title and content fields
setBlogTitle("");
setBlogContent(EditorState.createEmpty());
// Hide the alert after a delay
setTimeout(() => {
setAlert(false);
}, 3000);
} else {
// If title or content is missing
// Set form submission status and show a warning alert
setFormSubmitted(true);
setAlert(true);
// Hide the alert after a delay
setTimeout(() => {
setAlert(false);
}, 3000);
}
};
// Function triggered when the "Delete Blog" button is clicked
const onClickDelete = (e) => {
e.preventDefault();
onDeleteBlogFromBLogList({
blogID:searchParams.get("blogID")
});
};
// Function triggered when the "Update Blog" button is clicked
const onClickUpdate = (e) => {
e.preventDefault(); // Prevent the default form submission behavior
// Handling form submission
if (blogTitle && !isEditorEmpty(blogContent)) {
// If both title and content are provided
// Call the prop function to pass data
onUpdateDatatoAppComponent({
title: blogTitle,
content: draftToHtml(convertToRaw(blogContent.getCurrentContent())),
date: formatDateTime(currentDate),
blogID:searchParams.get("blogID")
});
// Reset form submission status
setFormSubmitted(false);
// Show success alert
setAlert(true);
// Clear title and content fields
setBlogTitle("");
setBlogContent(EditorState.createEmpty());
// Hide the alert after a delay
setTimeout(() => {
setAlert(false);
}, 3000);
} else {
// If title or content is missing
// Set form submission status and show a warning alert
setFormSubmitted(true);
setAlert(true);
// Hide the alert after a delay
setTimeout(() => {
setAlert(false);
}, 3000);
}
};
// Return JSX for rendering
return (
<Container>
<Row style={{ paddingTop: 90 }}>
<Form className="mt-3">
{/* Render the custom input text component */}
<CustomInputText
inputLabel={"Blog Title"}
inputPlaceholder={"Enter your Blog Title"}
inputType={"text"}
inputValidationMsg={"Please add Blog Title"}
inputValue={blogTitle}
inputOnChange={(e) => setBlogTitle(e.target.value)}
inputIsValid={formSubmitted && blogTitle === ""}
/>
{/* Render the editor component */}
<Form.Group className="mb-3" controlId="exampleForm.ControlTextarea1">
<Form.Label>Write your blog</Form.Label>
<Editor
defaultEditorState={blogContent}
editorState={blogContent}
onEditorStateChange={setBlogContent}
/>
<Form.Control.Feedback
style={{
display:
formSubmitted && isEditorEmpty(blogContent)
? "block"
: "none",
}}
type="invalid"
>
Please add blog content.
</Form.Control.Feedback>
</Form.Group>
{/* Render the alert component based on conditions */}
{isEditorEmpty(blogContent) && blogTitle === "" && formSubmitted ? (
<CustomAlert
isVisible={alert}
alertMessage="Please fill all required forms โ"
alertClass="danger"
/>
) : (
<CustomAlert
isVisible={alert}
alertMessage="Your blog publish successfully ๐"
alertClass="success"
/>
)}
{/* Render the custom button component */}
{searchParams.get("blogID") && blogList.length != 0 ?
<>
<CustomButton
buttonText={"Update Blog"}
buttonType={"submit"}
buttonClassName={"btn btn-custom w-100"}
buttonOnClick={(e) => onClickUpdate(e)}
/>
<CustomButton
buttonText={"Delete Blog"}
buttonType={"submit"}
buttonClassName={"btn btn-warning w-100 mt-3"}
buttonOnClick={(e) => onClickDelete(e)}
/>
</>
: <CustomButton
buttonText={"Publish Blog"}
buttonType={"submit"}
buttonClassName={"btn btn-custom w-100"}
buttonOnClick={(e) => onSubmitBtnClick(e)}
/>
}
</Form>
</Row>
</Container>
);
}
// Prop type validation for the "onPassDatatoAppComponent" prop
Blog.propTypes = {
onPassDatatoAppComponent: PropTypes.any,
blogList: PropTypes.any,
onUpdateDatatoAppComponent:PropTypes.any,
onDeleteBlogFromBLogList: PropTypes.any
};
// Export the "Blog" component as the default export
export default Blog;
Let's break down the code step by step:
Import Statements:
Import required libraries, components, and utilities.
Functional Component "Blog":
This is the main functional component that renders the blog creation, update, and deletion functionality.
State Management:
useState
hooks are used to manage various states, includingblogTitle
,blogContent
,formSubmitted
, andalert
.
Route Hooks:
The
useSearchParams
hook is used to access the query parameters from the URL.The
useNavigate
hook is used to navigate to different routes.
UseEffect:
This hook is used to handle side effects. In this case, it listens to changes in the
blogList
prop and updates the state accordingly.If the
blogList
is empty and a blog ID is provided in the query parameters, the component navigates to the "/blog" route.If a valid
blogId
is provided and theblogList
is not empty, the component sets the initial values forblogTitle
andblogContent
based on the selected blog.
Functions:
onSubmitBtnClick
: Triggered when the "Publish Blog" button is clicked. It handles form submission, validation, and alerts. It also uses theonPassDatatoAppComponent
prop to pass the blog data to the parent component.onClickDelete
: Triggered when the "Delete Blog" button is clicked. It handles the deletion of a blog using theonDeleteBlogFromBLogList
prop.onClickUpdate
: Triggered when the "Update Blog" button is clicked. It handles the update of a blog using theonUpdateDatatoAppComponent
prop.
Rendering JSX:
Renders various components such as
CustomInputText
,Editor
(fromreact-draft-wysiwyg
), andCustomButton
.Conditionally renders the "Update" and "Delete" buttons based on whether a valid
blogId
is provided in the query parameters and theblogList
is not empty.Displays alerts based on form submission status and validation.
PropTypes Validation:
- Validates the props using PropTypes to ensure they are of the expected types.
In summary, this component allows users to create, update, and delete blog posts. It uses hooks, state management, and various libraries to handle different aspects of blog management and rendering.
blogItem.jsx
import PropTypes from "prop-types";
import { propTypes } from "react-bootstrap/esm/Image";
function BlogItem({ blogTitle, blogAuthor, blogTimeAndDate, blogAuthorImage, onBlogClick }) {
console.log("Blog Item: ", blogTitle);
return (
<div className="blog-item" onClick={onBlogClick}>
<div className="blog-content">
<div className="blog-text">
<h3 className="blog-title">{blogTitle}</h3>
<p className="blog-author">{blogAuthor}</p>
<p className="blog-publish-time">{blogTimeAndDate}</p>
</div>
<div className="blog-img">
<div className="blog-img-bg">
<img
src={blogAuthorImage}
style={{ height: 55 }}
/>
</div>
</div>
</div>
</div>
);
}
BlogItem.propTypes = {
blogTitle: PropTypes.string.isRequired,
blogAuthor: PropTypes.string.isRequired,
blogTimeAndDate: PropTypes.string.isRequired,
blogAuthorImage: PropTypes.string.isRequired,
onBlogClick: propTypes.any
};
export default BlogItem;
Import Statements:
PropTypes
is imported from the"prop-types"
package. It's used for type checking of props.propTypes
is imported from"react-bootstrap/esm/Image"
. This import is unnecessary and can be removed, as it's not used in the code.
Functional Component "BlogItem":
This component represents an individual blog item.
It receives several props:
blogTitle
,blogAuthor
,blogTimeAndDate
,blogAuthorImage
, andonBlogClick
.The
console.log
statement is used to log theblogTitle
.
JSX Rendering:
The component's JSX structure represents the layout of a blog item.
The
div
withclassName="blog-item"
serves as a clickable container for the blog item.Inside the container, the blog's content is displayed.
The
onBlogClick
prop is set as theonClick
handler for the clickable container.
Content Display:
Blog data, such as
blogTitle
,blogAuthor
, andblogTimeAndDate
, is displayed using appropriate HTML elements (h3
,p
).The
blogAuthorImage
is displayed as animg
element with the given height.
Prop Type Validation:
BlogItem.propTypes
defines the expected types for each prop passed to the component.PropTypes.string.isRequired
specifies that theblogTitle
,blogAuthor
,blogTimeAndDate
, andblogAuthorImage
props must be strings and they are required.onBlogClick: propTypes.any
indicates that theonBlogClick
prop can be of any type.
Exporting:
- The
BlogItem
component is exported as the default export of this module.
- The
This code defines a BlogItem
component that represents an individual blog item. It receives various blog-related data as props and displays them using JSX. The component also includes prop type validation using PropTypes
.
homepage.jsx
import { Stack, Container, Row, Col, Form } from "react-bootstrap";
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
// Local Imports
import BlogItem from "../components/blogItem";
import "../css/homepage.css";
function Homepage({ blogListData }) {
const navigate = useNavigate();
console.log(blogListData);
const onBlogItemClick = (blogId) => {
navigate({pathname:'/blog', search:`?blogID=${blogId}`});
console.log("Blog Click");
}
return (
<div className="main-content">
<Container>
<section id="search-section">
<Row>
<Col>
<div className="searchfield">
<Form.Control type="text" placeholder="Search Blog" />
<i className="fa fa-search" aria-hidden="true"></i>
</div>
</Col>
</Row>
</section>
<section id="bloglist-title-section">
<div className="bloglist-title">
<h2>All Blogs</h2>
<i className="fa fa-file-text-o" aria-hidden="true"></i>
</div>
</section>
<section id="bloglist-section">
<Stack gap={3}>
{blogListData.length === 0 ? (
<p style={{ textAlign: "center" }}>No Blogs are available ๐</p>
) : (
blogListData.map((blog, i) => (
<BlogItem
key={i}
blogTitle={blog.title}
blogAuthor={"Pradhuman Padhiyar"}
blogTimeAndDate={blog.date}
blogAuthorImage={
"https://api.dicebear.com/6.x/open-peeps/svg?face=angryWithFang,calm,blank"
}
onBlogClick={() => onBlogItemClick(i)}
/>
))
)}
</Stack>
</section>
</Container>
</div>
);
}
Homepage.propTypes = {
blogListData: PropTypes.any,
};
export default Homepage;
Import Statements:
Import necessary components, libraries, and styles.
Import the
useNavigate
hook from"react-router-dom"
to handle navigation.
Functional Component "Homepage":
This component represents the homepage where blog items are displayed.
It receives the
blogListData
prop.
Navigating to Blog Page:
The
useNavigate
hook is used to get thenavigate
function, which allows redirection.The
onBlogItemClick
function is called when a blog item is clicked. It usesnavigate
to redirect to the blog page with a query parameter.
Rendering JSX:
The JSX structure represents the layout of the homepage.
It includes sections for search, blog list title, and the blog list itself.
Rendering Blog Items:
Within the "bloglist-section," a
Stack
component from"react-bootstrap"
is used to render the blog items.If there are no blogs available, a message is displayed.
If there are blogs available, the
blogListData
is mapped to render individualBlogItem
components.Each
BlogItem
receives various props includingonBlogClick
, which is triggered when the item is clicked.
Prop Type Validation:
Homepage.propTypes
defines the expected types for theblogListData
prop.
Exporting:
- The
Homepage
component is exported as the default export of this module.
- The
This code defines the Homepage
component that displays a list of blog items. It uses the useNavigate
hook to handle navigation when a blog item is clicked. The BlogItem
components are rendered with appropriate props, including the onBlogClick
function to navigate to the individual blog page.
app.jsx
import React, { useState } from "react";
import { Routes, Route, useNavigate } from "react-router-dom";
//Local Imports
import Header from "./components/header";
import Footer from "./components/footer";
import Homepage from "./pages/homepage";
import Blog from "./pages/blog";
import Profile from "./pages/profile";
import NoPageFound from "./pages/no_page_found";
import "./App.css";
function App() {
const [blogListData, setBlogListData] = useState([]);
const navigate = useNavigate();
const onGetDataFromBlogComponent = (blogData) => {
setBlogListData((prevDataArray) => [blogData, ...prevDataArray]);
console.log(blogData);
};
const onGetUpdatedDataFromBlogComponent = (blogData) => {
// setBlogListData([...blogListData.slice(0, blogData.blogID), blogData, ...blogListData.slice(blogData.blogID, blogListData.length)]);
let newState = [...blogListData];
newState[blogData.blogID] = blogData;
setBlogListData(newState)
console.log(blogData);
};
const onDeleteDataFromBlog = (blogId) => {
setBlogListData((prevState) =>
prevState.splice(blogId, 1)
);
navigate({pathname:'/'});
console.log(blogId);
};
return (
<React.Fragment>
<Header />
<Routes>
<Route path="/" element={<Homepage blogListData={blogListData} />} />
<Route
path="/blog/*"
element={
<Blog onPassDatatoAppComponent={onGetDataFromBlogComponent} blogList={blogListData} onUpdateDatatoAppComponent={onGetUpdatedDataFromBlogComponent} onDeleteBlogFromBLogList={onDeleteDataFromBlog}/>
}
/>
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<NoPageFound />} />
</Routes>
<Footer />
</React.Fragment>
);
}
export default App;
Import Statements:
Import necessary components, hooks, and styles.
Import components like
Header
,Footer
,Homepage
,Blog
,Profile
, andNoPageFound
from respective files.
Functional Component "App":
This is the main component representing the whole application.
Manages the state of
blogListData
using theuseState
hook.It also uses the
useNavigate
hook to handle navigation.
Functions for Handling Blog Data:
onGetDataFromBlogComponent
: Adds new blog data to the list by updating the state.onGetUpdatedDataFromBlogComponent
: Updates existing blog data in the list using the updated data'sblogID
.onDeleteDataFromBlog
: Deletes blog data from the list using theblogId
and navigates back to the homepage.
Routing:
Utilizes the
Routes
component to define routes.Route paths are defined using
Route
components.Props like
blogListData
, functions for passing data, updating data, and deleting data are passed to theBlog
andHomepage
components.
Rendering:
The JSX structure defines the layout of the app.
It includes rendering the
Header
, routing using theRoutes
component, and rendering theFooter
.
Exporting:
- The
App
component is exported as the default export of this module.
- The
This code defines the main App
component that manages state and routing for the entire application. It utilizes the useState
and useNavigate
hooks to handle state and navigation, respectively. It also manages the data flow between different components using various functions passed as props.
The Routes
component is used for routing, and specific components are rendered based on the designted routes.
You can get all this code from github repository
Run your APP
Simply run the following command in your terminal:
npm run dev
Now, give yourself a pat on the back, as you've just implemented useEffect, Edit, Update, and Delete blog features in our React app.
So, that concludes this blog post.
What's Next?
In the next blog, we will implement a feature that allows us to search for blogs and introduce a profile feature where we utilize the Formik and Yup packages for form validation and handling. We will learn more about storing data in local storage and saving profile data in local storage.
Conclusion
In this blog post, we covered how to edit, update, and delete blogs in a React app, working with dynamic routes and handling navigation using the React Router DOM. We also explored the useEffect
hook and its role in managing side effects in functional components.
Stay tuned for the next blog post, where we'll implement a search feature and introduce a profile feature using Formik and Yup for form validation and handling, as well as working with local storage.
Also, Follow me on Twitter or Linkedin I share about Cloud, System Design, Code, and many more tech stuff ๐
Resources
Subscribe to my newsletter
Read articles from Pradhumansinh Padhiyar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Pradhumansinh Padhiyar
Pradhumansinh Padhiyar
Sharing what I learn about Cloud, System design, and Code โข On my journey to becoming a certified AWS Dev โข Writing ebook on System Design