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

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 and useEffect.

  • 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 the count 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 the count 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 the useEffect 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:

  1. Import Statements:

    Import required libraries, components, and utilities.

  2. Functional Component "Blog":

    This is the main functional component that renders the blog creation, update, and deletion functionality.

  3. State Management:

    • useState hooks are used to manage various states, including blogTitle, blogContent, formSubmitted, and alert.
  4. 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.

  5. 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 the blogList is not empty, the component sets the initial values for blogTitle and blogContent based on the selected blog.

  6. Functions:

    • onSubmitBtnClick: Triggered when the "Publish Blog" button is clicked. It handles form submission, validation, and alerts. It also uses the onPassDatatoAppComponent 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 the onDeleteBlogFromBLogList prop.

    • onClickUpdate: Triggered when the "Update Blog" button is clicked. It handles the update of a blog using the onUpdateDatatoAppComponent prop.

  7. Rendering JSX:

    • Renders various components such as CustomInputText, Editor (from react-draft-wysiwyg), and CustomButton.

    • Conditionally renders the "Update" and "Delete" buttons based on whether a valid blogId is provided in the query parameters and the blogList is not empty.

    • Displays alerts based on form submission status and validation.

  8. 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;
  1. 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.

  2. Functional Component "BlogItem":

    • This component represents an individual blog item.

    • It receives several props: blogTitle, blogAuthor, blogTimeAndDate, blogAuthorImage, and onBlogClick.

    • The console.log statement is used to log the blogTitle.

  3. JSX Rendering:

    • The component's JSX structure represents the layout of a blog item.

    • The div with className="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 the onClick handler for the clickable container.

  4. Content Display:

    • Blog data, such as blogTitle, blogAuthor, and blogTimeAndDate, is displayed using appropriate HTML elements (h3, p).

    • The blogAuthorImage is displayed as an img element with the given height.

  5. Prop Type Validation:

    • BlogItem.propTypes defines the expected types for each prop passed to the component.

    • PropTypes.string.isRequired specifies that the blogTitle, blogAuthor, blogTimeAndDate, and blogAuthorImage props must be strings and they are required.

    • onBlogClick: propTypes.any indicates that the onBlogClick prop can be of any type.

  6. Exporting:

    • The BlogItem component is exported as the default export of this module.

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;
  1. Import Statements:

    • Import necessary components, libraries, and styles.

    • Import the useNavigate hook from "react-router-dom" to handle navigation.

  2. Functional Component "Homepage":

    • This component represents the homepage where blog items are displayed.

    • It receives the blogListData prop.

  3. Navigating to Blog Page:

    • The useNavigate hook is used to get the navigate function, which allows redirection.

    • The onBlogItemClick function is called when a blog item is clicked. It uses navigate to redirect to the blog page with a query parameter.

  4. Rendering JSX:

    • The JSX structure represents the layout of the homepage.

    • It includes sections for search, blog list title, and the blog list itself.

  5. 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 individual BlogItem components.

    • Each BlogItem receives various props including onBlogClick, which is triggered when the item is clicked.

  6. Prop Type Validation:

    • Homepage.propTypes defines the expected types for the blogListData prop.
  7. Exporting:

    • The Homepage component is exported as the default export of this module.

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;
  1. Import Statements:

    • Import necessary components, hooks, and styles.

    • Import components like Header, Footer, Homepage, Blog, Profile, and NoPageFound from respective files.

  2. Functional Component "App":

    • This is the main component representing the whole application.

    • Manages the state of blogListData using the useState hook.

    • It also uses the useNavigate hook to handle navigation.

  3. 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's blogID.

    • onDeleteDataFromBlog: Deletes blog data from the list using the blogId and navigates back to the homepage.

  4. 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 the Blog and Homepage components.

  5. Rendering:

    • The JSX structure defines the layout of the app.

    • It includes rendering the Header, routing using the Routes component, and rendering the Footer.

  6. Exporting:

    • The App component is exported as the default export of this module.

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

4
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