Learn Modern React and Redux in 2023 by Building a Blog

Redux is a popular state management library for JavaScript applications. State management is very important in modern web applications. It controls what information is shown on the screen as a user interacts with the app. Basically, it keeps track of everything that is happening in the app and decides what to show to the user.

Good state management is essential for creating web applications that work well and provide a great user experience. Without it, things can become confusing and messy, and the user might not know what is going on. By managing the state of the application properly, developers can ensure that the user always sees up-to-date information and that everything runs smoothly. It makes the app more reliable and easier to use. In short, state management is one of the vital aspects of creating a successful web application. In order to effectively manage the state of a web application, developers use various techniques and tools. One common method is to use a state management library such as Redux.

Redux helps manage the state of an application in a more organized and predictable way. It provides a predictable state container for JavaScript applications. This container holds all application states in a central location, making it easy to manage and update.

What we are building

In this tutorial, you will learn how to create a frontend blog application with various features, including navigation, post creation, editing, deletion, upvoting and downvoting, and adding posts to favorites using React and the latest version of Redux Toolkit.

You can view the finished application by following this link: https://modern-react-redux-blog.vercel.app, while the GitHub repository containing the source code can be found at: https://github.com/bejamas/react-redux-blog

Prerequisites

In order to proceed with this tutorial, there are specific tools that you should be familiar with. Firstly, you should have a basic understanding of HTML and CSS. Secondly, it’s important to be familiar with the syntax and features of ES6. Additionally, having knowledge of key terms related to React such as JSX, State, Function Components, Props, and Hooks is essential. Lastly, it would be helpful to have experience with Type Annotations in TypeScript, as TypeScript is the language that will be used in this project. By having this foundational knowledge, you will be able to better understand and implement the concepts presented in the tutorial.

Setting up the environment

We will be using Vite to bootstrap this project. Vitejs is an ultra-fast and modern web development build tool and development server. It is built on top of modern web technologies such as ES Modules and leverages browser native support for ES Modules to enable lightning-fast development and build times.

Configure the project with Vite

Create a new folder called react-redux-blog and run the following command to setup the project with Vite:

npm create vite@latest

You’ll be asked for the name of your project, simply type: ./ to use the name of the folder (react-redux-blog) that was created previously.

Next, you will be asked to “Select a framework”, use the arrow key to navigate to the React option, and press the Enter key.

Finally, you will be asked to “Select a variant”, select TypeScript, and press the Enter key.

Execute the command below to install React and all other dependencies that have been configured with Vite in the package.json file:

npm install

Once the installation is complete, you can run the following command to start your development server:

npm run dev

Now launch your browser and navigate to the following link: http://localhost:5174/.

You’ll see a page similar with a basic counter app that looks like this:

Redux Toolkit vs. Redux Core

Redux core is the original and old library for state management with Redux. It provides the core concepts such as Store, Actions, Reducers, and Middleware, but it requires more boilerplate code to set up and configure. Developers need to manually handle things like action types, action creators, and reducers, which can result in more code and potential errors.

Redux Toolkit (Also referred to as RTK), on the other hand is the new version of Redux and the officially recommended approach for writing Redux logic. It automatically generates action creator functions for each reducer and generates action type strings internally based on the reducer’s names. Redux Toolkit offers excellent TS support, with APIs designed to give you excellent type safety and minimize the number of types you have to define in your code.

Moreover, with RTK, you can write mutating logic without the risk of actual mutation. Its can help avoid unexpected behavior in your application. We will delve more into this later in the tutorial.

RTK will be used for building the application in this tutorial.

Installing the Redux Toolkits and React-Redux

The Redux Toolkit is composed of the Redux core, along with other essential packages like Reselect and Redux Thunk, which are deemed vital for developing Redux applications.

React Redux offers bindings that enable React components to communicate with the Redux store.

Run the following command to install both of them:

npm install @reduxjs/toolkit react-redux

Creating and configuring the store

A store is an object that holds the global state of our application. To create the store for this app, navigate to the src directory and create a new folder called app, then create a new file called store.ts inside the app directory.

Open the store.ts file and add the following code to it:

import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {},
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

Above, we defined a Redux store by calling the configureStore function and passing an empty reducer object as an argument. The store is saved into the store variable, which is exported for later use.

Next, we will use the Provider component from the react-redux library to connect the React component to the store object that was exported from the previous code.

Open the main.ts file and update it with the following code:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { Provider } from 'react-redux';
import { store } from './app/store';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)

In the code above, we used the Provider component to connect our app to the store so that all the components can access it within the application.

Creating the components

In this part, we will create all the components that make up the app we are building. Let’s start by replacing the entire content of the src/index.css file with the following code:

body {
	margin: 0;
	background-color: #f4f9fd;
	font-family: Arial, Helvetica, sans-serif;
	min-height: 100vh;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.main-container {
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	min-height: 100vh;
}

nav {
	width: 90%;
	border-radius: 10px;
	background-color: #fff;
	box-shadow: rgba(0, 0, 255, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	margin: 20px auto;
	padding: 0 10px;
	display: flex;
	align-items: center;
	justify-content: space-between;
	overflow-x: auto;
}

.right-nav-section,
.left-nav-section {
	display: flex;
	align-items: center;
}

.left-nav-section {
	margin-right: 20px;
}

.logo {
	display: flex;
	flex-direction: column;
	padding: 10px;
}

.logo h1,
.logo h2 {
	margin: 0;
	font-size: 15px;
}

.logo h1 {
	font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
	margin-right: 10px;
}

.logo h2 {
	font-weight: 100;
}

.divider {
	font-size: 20px;
	margin: 0;
	font-weight: 100;
	color: rgba(0, 0, 0, 0);
	border-right: 1px solid rgba(0, 0, 0, 0.321);
}

.nav-item {
	margin: 0 0 0 20px;
	font-size: 14px;
	font-weight: 600;
	color: rgba(0, 0, 0, 0.5);
	cursor: pointer;
	display: flex;
	align-items: center;
}

.nav-item span {
	margin-right: 5px;
}

.add-icon {
	padding: 0 4px;
	border-radius: 50%;
	margin-right: 5px;
	color: white;
	background-color: red;
	font-weight: 700;
	font-size: 15px;
}

.active {
	color: #2c48f3;
}


.card-container {
	display: flex;
	align-items: center;
	justify-content: center;
	flex-wrap: wrap;
}

.card,
.no-posts-card {
	width: 250px;
	height: 250px;
	display: flex;
	flex-direction: column;
	text-align: left;
	padding: 15px;
	justify-content: space-between;
	background-color: #fff;
	border-radius: 10px;
	box-shadow: rgba(0, 0, 255, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	margin: 10px;
}

.no-posts-card {
	justify-content: center;
	align-items: center;
	text-align: center;
}

.no-posts-card button {
	padding: 10px 20px;
	color: #fff;
	background-color: #2c48f3;
	border: none;
	font-weight: 600;
	box-shadow: rgba(0, 0, 255, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	cursor: pointer;
}

.no-posts-card h1 {
	font-size: 17px;
}

.card-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
}

.edit-icon {
	font-size: 16px;
	color: rgba(0, 0, 0, 0.801);
	cursor: pointer;
	padding: 0 0;
	border: 3px double rgba(0, 0, 0, 0.788);
	font-weight: bold;
	box-shadow: rgba(47, 0, 255, 0.55) 0 2px 5px -1px, rgba(0, 0, 0, 0.5) 0 1px 3px -1px;
}

.delete-icon {
	margin: 0;
	background-color: #2c48f3;
	border-radius: 50%;
	height: 22px;
	width: 22px;
	text-align: center;
	font-size: 17px;
	color: #fff;
	box-shadow: rgba(0, 0, 255, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	cursor: pointer;
}

.card-body {
	overflow: hidden;
	padding: 10px 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: space-between;
}

.card-bottom {
	display: flex;
	justify-content: space-between;
	align-items: center;
}


.favorite {
	font-size: 30px;
	color: #2600ff;
	cursor: pointer;
	text-shadow: 1px 1px 3px #2c48f3;
}

.rating-section {
	display: flex;
	align-items: center;
}

.upvote,
.downvote {
	color: #fff;
	font-size: 22px;
	background-color: #2c48f3;
	padding: 0 15px 5px 15px;
	margin: 0;
	border-radius: 5px;
	box-shadow: rgba(0, 0, 255, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	cursor: pointer;
	text-align: center;
	font-weight: 900;
}

.vote-counter {
	font-size: 15px;
	padding: 5px;
	font-weight: 600;

}

.card-title,
.card-summary {
	margin: 0;
	cursor: pointer;
}

.card-title {
	color: rgba(0, 0, 0, 0.7);
}

.card-summary {
	font-size: 15px;
	font-weight: 200;
	color: rgba(0, 0, 0, 0.6);
	margin-top: 10px;
	font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
}

.view-post-container {
	width: 80%;
	min-height: 80vh;
	margin: 0 auto;
	background-color: #fff;
	border-radius: 10px;
	box-shadow: rgba(0, 0, 255, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	margin: 10px;
	padding: 40px;
}

.view-post-title {
	font-size: 30px;
	text-align: center;
	text-transform: capitalize;
}

.view-post-content {
	font-size: 25px;
	text-align: left;
	font-weight: 100;
	line-height: 40px;
	font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
}


.form-card {
	width: 60%;
	height: 400px;
	text-align: center;
	padding: 40px;
	background-color: #fff;
	border-radius: 10px;
	box-shadow: rgba(0, 0, 255, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	margin: 10px auto;
	display: flex;
	flex-direction: column;
	align-items: center;
}

.title-input {
	width: 100%;
	font-size: 20px;
	padding: 10px 10px;
	outline: none;
}

.content-input {
	width: 100%;
	height: 200px;
	margin-top: 10px;
	font-size: 20px;
	font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
	outline: none;
	padding: 10px 10px;
}

.post-button {
	width: 100px;
	margin-top: 10px;
	font-size: 20px;
	background-color: #2c48f3;
	padding: 10px;
	color: #fff;
	border: none;
	cursor: pointer;
	box-shadow: rgba(0, 0, 0, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
}


footer {
	width: 50%;
	border-radius: 10px;
	background-color: #000;
	box-shadow: rgba(0, 0, 0, 0.25) 0 2px 5px -1px, rgba(66, 4, 236, 0.3) 0 1px 3px -1px;
	margin: 20px auto;
	padding: 0 10px;

}

footer p {
	color: #fff;
	text-align: center;
	font-size: 14px;
	font-weight: 500;
}

@media only screen and (max-width: 450px) {

	.title-input {
		font-size: 15px;
	}

	.content-input {
		font-size: 15px;
	}
}

@media only screen and (max-width: 300px) {

	.nav-item {
		margin: 0 0 0 15px;
		font-size: 10px;
		font-weight: 200;
	}

	.add-icon {
		padding: 0 4px;
		border-radius: 50%;
		margin-right: 2px;
		color: white;
		font-weight: 700;
		font-size: 16px;
	}

	.left-nav-section {
		margin-right: 0;
	}

	footer {
		width: 70%;
	}
}

The NavigationBar component

In the src directory, create a new folder called features. Redux recommends organizing the logic of the app into the features directory. In the feature directory, create a folder called navigations. The navigations folder will contain the components related to the app navigation, as well as a single file that uses the createSlice API for the navigation feature (navigationSlice which will be created later).

The navigations folder will house the components responsible for the app’s navigation, along with a single file that uses the createSlice API for the navigation feature (navigationSlice, which will be created at a later stage).

Now, we are ready to create the NavigationBar component. To do this, create a new file named NavigationBar.tsx inside the features directory and add the following code to it:

const NavigationBar = () => {
    return (
      <>
        <nav>
          <section className="right-nav-section">
            <div className="logo">
              <h1>Redux</h1>
              <h2>Blog</h2>
            </div>
            <p className="divider">|</p>

            <p className="nav-item">Blogs</p>

            <p className="nav-item">
              <span className="add-icon">+</span>Add New
            </p>
          </section>
          <section className="left-nav-section">
            <p className="divider">|</p>

            <p className="nav-item">
              <span></span>Favorites
            </p>
          </section>
        </nav>
      </>
    );
  };
  export default NavigationBar;

We defined a functional component called NavigationBar that renders a navigation bar for our app, with Blogs and Add New navigation items on the right-hand side and a Favorites navigation item on the left-hand side.

The AddNewPost component

To add a new post, we will create a new component called AddNewPost. However, before proceeding with the creation of the component, we must first create a typing file that will be used to define the structure of the new post. Start by creating a directory named typings in the src folder, then create a new file named index.ts. You can then include the code below within the file:

export interface IPost {
    id: number,
    title: string,
    content: string,
    voteCount:number,
    isFavorite: boolean,
}

We defined a new TypeScript interface called IPost which has five properties: id, title, content, voteCount, and isFavorite. The id and voteCount properties are numbers, the title and content properties are strings and the isFavorite property is a boolean.

In the features directory, create a new folder called posts. Inside the posts folder, create a new file called PostInputForm.tsx, and add the following code to it:

import { FormEventHandler } from "react";

const PostInputForm = (props: {
  submitPost: FormEventHandler<HTMLFormElement>;
  title: string;
  setTitle: (title: string) => void;
  content: string;
  setContent: (content: string) => void;
}) => {
  return (
    <>
      <form className="form-card" onSubmit={props.submitPost}>
        <h1>Create a new Post</h1>
        <input
          type="text"
          className="title-input"
          value={props.title}
          onChange={(e) => props.setTitle(e.target.value)}
        />
        <textarea
          className="content-input"
          value={props.content}
          onChange={(e) => props.setContent(e.target.value)}
        ></textarea>
        <input type="submit" value="Publish" className="post-button" />
      </form>
    </>
  );
};
export default PostInputForm;

We defined a new component called PostInputForm that renders a form for creating a new post. The form includes an input field for the title, a text area for the content, and a submit button. This component will also be used whenever a post needs to be modified.

Create a new file in the features/posts directory called AddNewPost.tsx and then add the following code to it:

import { useState } from "react";
import { IPost } from "../../typings";
import PostInputForm from "./PostInputForm";

const AddNewPost = () => {
  const [title, setTitle] = useState("Blog Title");
  const [content, setContent] = useState("Blog Details");

  const submitPost = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const post: IPost = {
      id: Date.now(),
      title,
      content,
      voteCount: 0,
      isFavorite: false,
    };
    console.log(post);
  };
  return (
    <>
      <PostInputForm
        submitPost={submitPost}
        title={title}
        content={content}
        setTitle={setTitle}
        setContent={setContent}
      />
    </>
  );
};
export default AddNewPost;

We created a component called AddNewPost that renders a form for creating a new post, using the imported PostInputForm component that was created previously. The component uses the useState hook to create two state variables - title and content, which are initialized with default values “Blog Title” and “Blog Details”. The component also defines a function called submitPost which is passed as a prop to the PostInputForm component.

We will handle the submitPost function later in this tutorial.

The BlogCard component

Each blog content on the homepage will be rendered in a card. In this part, we will create a new component that will be used to render each post on the homepage. In the features/posts directory, create a new file called BlogCard.tsx and add the following code to it:

const BlogCard = (props: { posts: any }) => {
  return (
    <>
      {props.posts.map((post: any) => {
        return (
          <section className="card" key={post.id}>
            <div className="card-header">
              <span className="edit-icon"></span>
              <h1 className="delete-icon">x</h1>
            </div>
            <section className="card-body">
              <h1 className="card-title">
                {post.title.length > 150
                  ? post.title.slice(0, 150) + "..."
                  : post.title}
              </h1>
              <p className="card-summary">
                {post.content.length > 120
                  ? post.content.slice(0, 120) + "..."
                  : post.content}
              </p>
            </section>
            <section className="card-bottom">
              <section>
                <span
                  className="favorite"
                >
                </span>
              </section>
              <section className="rating-section">
                <span className="upvote"></span>
                <span className="vote-counter">0</span>
                <span className="downvote"></span>
              </section>
            </section>
          </section>
        );
      })}
    </>
  );
};

export default BlogCard;

This component takes in a single prop called posts, which is expected to be an array of post objects, then uses the map function to iterate over the post array and render a card element for each post.

Each card element includes a header section, a body section, and a bottom section. The header section has a pencil icon to edit a post and a cross icon to delete a post. The body section includes the post’s title, truncated to 150 characters with an ellipsis if it’s longer, and the post’s content, truncated to 120 characters with an ellipsis if it’s longer. The bottom section has a heart icon, which will be used to add or remove a post from favorites, and a section for voting, which has an upvote arrow, a vote counter, and a downvote arrow. Each card also has a key prop which is set to the post’s id.

The NoPosts component

The NoPosts component will be rendered whenever the post array is empty. In the component features/posts directory, create a new file called NoPosts.tsx and add the following code to it:

const NoPosts = () => {
  return (
    <>
      <section className="no-posts-card">
        <h1>You have not created any blog posts.</h1>
        <button>Add New</button>
      </section>
    </>
  );
};
export default NoPosts;

This component will render a card that indicates that the user has not created any blog posts. The component has a heading tag that says “You have not created any blog posts” and a button that says “Add New”. The button will be handled later in this tutorial.

The AllPosts component

This component will be used to render all the user posts. Create a new file called AllPosts.tsx in the features/posts directory, and add the following code to it:

import BlogCard from "./BlogCard";
import NoPosts from "./NoPosts";

const AllPosts = () => {
  const allPosts: any = [];
  if (!allPosts.length) {
    return <NoPosts />;
  }
  return (
    <>
      <BlogCard posts={allPosts} />
    </>
  );
};
export default AllPosts;

We created a new component called AllPosts that will be used to render a list of all blog posts. The component imports BlogCard and NoPosts components. If the blog post array is empty, it renders the NoPosts component. Otherwise, it returns the BlogCard component.

The EditPost component

The EditPost component will be rendered anytime an existing post needs to be modified. In the features/posts directory, create a new file called EditPost.tsx and add the following code:

import { useState } from "react";
import { IPost } from "../../typings";
import PostInputForm from "./PostInputForm";

const EditPost = () => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const updatePost = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const post: IPost = {
      id: Date.now(),
      title,
      content,
      voteCount: 0,
      isFavorite: false,
    };
    console.log(post);
  };
  return (
    <>
      <PostInputForm
        submitPost={updatePost}
        title={title}
        content={content}
        setTitle={setTitle}
        setContent={setContent}
      />
    </>
  );
};
export default EditPost;

We created a new component called EditPost. For now, we initialize the two state variables - title and content, with empty strings as their initial values. We also define a function called updatePost which is triggered whenever the form is submitted. We will handle this function later in this tutorial.

The NoFavoritePosts component

Let’s create another component called NoFavoritePosts. This component will be rendered on the favorites page if no post has been added to the favorites list.

Create another file in the features/posts directory called NoFavoritePosts.tsx and add the following code to it:

const NoFavoritePosts = () => {
  return (
    <>
      <section className="no-posts-card">
        <h1>You haven't marked any blog posts as favorites.</h1>
        <button>Add a post to your favorites</button>
      </section>
    </>
  );
};
export default NoFavoritePosts;

The FavoritePosts component

The FavoritePosts component will be used to display every post that has been marked as a favorite. Create a new file in the features/posts directory called FavoritePosts.tsx and add the following code to it:

import BlogCard from "./BlogCard";
import NoFavoritePosts from "./NoFavoritePosts";

const FavoritePosts = () => {
  const favoritePosts: any[] = [];
  if (!favoritePosts.length) {
    return <NoFavoritePosts />;
  }
  return (
    <>
      <BlogCard posts={favoritePosts} />
    </>
  );
};
export default FavoritePosts;

The ViewPostDetails component

The ViewPostDetails component will be used to view the details of a selected blog post. To create the ViewPostDetails component, we will create a new file in the features/posts directory called ViewPostDetails.tsx and add the following code to it:

const ViewPostDetails = () => {
  const post = { title: "", content: "" };
  return (
    <>
      <section className="view-post-container">
        <h1 className="view-post-title">{post.title}</h1>
        <p className="view-post-content">{post.content}</p>
      </section>
    </>
  );
};
export default ViewPostDetails;

The Footer Component

In the src/fearures/posts file, create a new component called Footer.tsx open it, and add the following code to it:

const Footer = () => {
  return (
    <>
      <footer>
        <p>
          Made with ❤ by
          <a href="<https://github.com/tope-olajide>">Temitope.js</a>
        </p>
      </footer>
    </>
  );
};
export default Footer;

Implementing the navigation feature

What is a slice in Redux?

Let’s gain some understanding of what a slice is before we begin creating the necessary slices for this application.

Slices is a powerful feature of Redux that make it easier to manage complex states, write clean and modular code, and avoid common pitfalls of state management. It is created using the createSlice function provided by Redux Toolkit, which generates a reducer function, actions, and action creators for the slice.

Slices help simplify the process of writing Redux logic by allowing you to work with smaller parts of your state tree instead of having to manage the entire state at once. It can make your code more modular, easier to test, and reduce the risk of errors caused by unintentional state mutations. Each slice has a unique name and a set of reducers that specifies how the slice should be updated when an action is dispatched. Actions generated by a slice are automatically assigned a type that includes the slice’s name, making it easier to track which slice is responsible for a particular action.

Creating the navigation slice

Before creating the navigation slice, let’s create a new type called INavigationState below the IPost interface in the typings/index.ts file:

export interface INavigationState {
    currentComponent:
      | "AllPostsComponent"
      | "AddNewPostComponent"
      | "FavoriteComponent"
      | "EditPostComponent"
      | "ViewPostComponent";
  }

We defined a new TypeScript interface called INavigationState that defines a property called currentComponent. The currentComponent property can have one of five possible string values: AllPostsComponent, AddNewPostComponent, FavoriteComponent, EditPostComponent, or ViewPostComponent.

After that, we will create a new file in src/features/navigations called navigationSlice.tsx and add the following code to it:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { INavigationState } from "../../typings";

const initialState: INavigationState = {
  currentComponent: "AllPostsComponent",
};

export const navigationSlice = createSlice({
  name: "navigations",
  initialState: initialState,
  reducers: {
    switchToAllPostsComponent: (state) => {
      state.currentComponent = "AllPostsComponent";
    },
    switchToAddNewPostComponent: (state) => {
      state.currentComponent = "AddNewPostComponent";
    },
    switchToFavoriteComponent: (state) => {
      state.currentComponent = "FavoriteComponent";
    },
    switchToEditPostComponent: (state) => {
      state.currentComponent = "EditPostComponent";
    },
    switchToViewPostComponent: (state) => {
      state.currentComponent = "ViewPostComponent";
    },
  },
});

export const {
  switchToAllPostsComponent,
  switchToAddNewPostComponent,
  switchToFavoriteComponent,
  switchToEditPostComponent,
  switchToViewPostComponent,
} = navigationSlice.actions;

export const selectCurrentComponent = (state: RootState) =>
  state.navigation.currentComponent;

export default navigationSlice.reducer;

In the code above, we created a Redux slice that will be used to handle the navigation in our app.

We used the createSlice function from the “reduxjs/toolkit” library to create the navigationSlice, which takes in the initial state (i.e., initialState), an object of reducer functions (i.e., switchToAllPostsComponent, switchToAddNewPostComponent, switchToFavoriteComponent, switchToEditPostComponent, and switchToViewPostComponent) and a “slice name” (i.e., navigations) then we exported the action creators automatically generated by the navigationSlice function and the selector function (i.e., selectCurrentComponent) that can be used to access the current component (i.e., currentComponent) from the global state.

With the createSlice function in Redux, we can write reducers that appear to mutate the state but actually don’t. It is because createSlice uses the Immer library to detect changes to a “draft state” and creates a new immutable state based on those changes. So, we get the benefit of writing reducer logic that looks like it’s mutating the state but without the actual mutations that can lead to unexpected behavior in our application.

Open the src/app/store.ts file, import the navigation reducer exported from the navigationSlice function, and then pass it to the store object like this:

import { configureStore } from '@reduxjs/toolkit';
+ import navigationReducer from '../features/navigations/navigationSlice'

export const store = configureStore({
+  reducer: {navigation: navigationReducer},
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

useDispatch and useSelector hooks

The useDispatch and useSelector are two hooks provided by the react-redux library that enable components in a React application to interact with the global Redux store.

The useDispatch hook is used to dispatch actions to the Redux store. When an action is dispatched, it triggers a state update in the store, which in turn causes React components that depend on the changed state to re-render.

The useSelector hook is used to select a part of the state from the Redux store. It takes a function that returns a slice of the Redux store’s state and returns that slice of state to the component that called it.

In the src/apps directory, create a new file called hooks.ts, open it, and add the following code:

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

We defined two new hooks that wrap the original useDispatch and useSelector hooks. These new hooks are called useAppDispatch and useAppSelector. The useAppDispatch hook returns an instance of the useDispatch hook with the AppDispatch type specified as the generic argument. The AppDispatch type represents the type of the dispatch function returned by the Redux store.

The useAppSelector hook is a strongly-typed version of the useSelector hook that takes the RootState type as an argument. It ensures that the selected state is of the correct type and avoids type errors.

These new hooks will be used throughout the app instead of the original useDispatch and useSelector hooks to provide stronger type safety and to ensure that the correct types are used.

Open the src/features/navigations/NavigationBar.tsx file and overwrite everything in the NavigationBar component with the following code:


import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
  selectCurrentComponent,
  switchToAllPostsComponent,
  switchToAddNewPostComponent,
  switchToFavoriteComponent,
} from "./navigationSlice";

const NavigationBar = () => {
  const dispatch = useAppDispatch();
  const currentComponent = useAppSelector(selectCurrentComponent);
  return (
    <>
      <nav>
        <section className="right-nav-section">
          <div className="logo"  onClick={() => dispatch(switchToAllPostsComponent())}>
            <h1>Redux</h1>
            <h2>Blog</h2>
          </div>

          <p className="divider">|</p>

          <p
            className={
              currentComponent === "AllPostsComponent"
                ? "nav-item active"
                : "nav-item"
            }
            onClick={() => dispatch(switchToAllPostsComponent())}
          >
            Blogs
          </p>

          <p
            className={
              currentComponent === "AddNewPostComponent"
                ? "nav-item active"
                : "nav-item"
            }
            onClick={() => dispatch(switchToAddNewPostComponent())}
          >
            <span className="add-icon">+</span>New
          </p>
        </section>
        <section className="left-nav-section">
          <p className="divider">|</p>

          <p
            className={
              currentComponent === "FavoriteComponent"
                ? "nav-item active"
                : "nav-item"
            }
            onClick={() => dispatch(switchToFavoriteComponent())}
          >
            <span></span>Favorites
          </p>
        </section>
      </nav>
    </>
  );
};
export default NavigationBar;

We first imported the custom hooks useAppDispatch and useAppSelector that we had previously created in the hooks file.

When a navigation link is clicked, the useAppDispatch function will be used to dispatch an action to the store to change the current component.

The useAppSelector hook retrieves the current component from the store and assigns it to the currentComponent variable. Depending on the value of currentComponent, the color of the current navigation item will be changed to blue. For instance, if currentComponent is equal to “AllPostsComponent”, the “nav-item active” class will be rendered, otherwise only the “nav-item” class will be rendered.

To view all the components in action, replace the contents of the App.tsx file with the following code:

import { useAppSelector, useAppDispatch } from "./app/hooks";
import AddNewPost from "./features/posts/AddNewPost";
import AllPosts from "./features/posts/AllPosts";
import EditPost from "./features/posts/EditPost";
import FavoritePosts from "./features/posts/FavoritePosts";
import Footer from "./features/posts/Footer";
import NavigationBar from "./features/navigations/NavigationBar";
import { selectCurrentComponent } from "./features/navigations/navigationSlice";
import ViewPostDetails from "./features/posts/ViewPostDetails";

const App = () => {
  const currentComponent = useAppSelector(selectCurrentComponent);

  return (
    <>
      <section className="main-container">
        <NavigationBar />
        <section className="card-container">
          {currentComponent === "AllPostsComponent" ? (
            <AllPosts />
          ) : currentComponent === "EditPostComponent" ? (
            <EditPost />
          ) : currentComponent === "ViewPostComponent" ? (
            <ViewPostDetails />
          ) : currentComponent === "AddNewPostComponent" ? (
            <AddNewPost />
          ) : currentComponent === "FavoriteComponent" ? (
            <FavoritePosts />
          ) : null}
        </section>
        <Footer />
      </section>
    </>
  );
};
export default App;

We used a series of conditional statements (ternary operators) to render the appropriate component based on the value of currentComponent. For example, we render the AllPosts component if currentComponent is equal to “AllPostsComponent”, EditPost component if currentComponent is equal “EditPostComponent” and so on. If none of the conditions are met, we return null. Also, we render NavigationBar and Footer components, which are displayed on every screen. The rendered component is wrapped in a <section className="card-container"> element.

Create a new post

A component has been created for accepting new post input, however, the feature to save the post into the state is yet to be implemented.

Let’s create a new file called postSlice.ts in the src/features/posts directory and add the following code to it:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { IPost } from "../../typings";

interface IAllPosts {
  allPosts: IPost[];
}

const initialState:IAllPosts =  { allPosts : [] };

export const postSlice = createSlice({
    name: 'all-posts',
    initialState: initialState,
    reducers: {
        saveBlogPost: (state, actions:{payload:IPost}) => {
            state.allPosts.push(actions.payload);
        }
    }
})

export const selectAllPosts = (state: RootState) => state.posts;

export const { saveBlogPost } = postSlice.actions;

export default postSlice.reducer;

In the code above, we defined the initialState, which is an object with a property allPosts that is an array of blog posts. Then we create the postSlice function using the createSlice function from the @reduxjs/toolkit library. The name of the postSlice function is “all-posts”. The postSlice function has a reducer called saveBlogPost, which will be dispatched anytime we need to save a new post. The saveBlogPost reducer takes the state and an action object as its arguments. The action object has a payload property containing the new post that will be pushed into the state array.

We also create a selector function called selectAllPosts, which takes the whole state as an argument and returns the posts property of it. We will call the selectAllPosts anytime we need to fetch all the posts. Lastly, the slice exports the postSlice.reducer which is the generated reducer function as the default export.

In the src/app/store.ts file, import the post reducer, and add it to the store object:

import { configureStore } from '@reduxjs/toolkit';
import navigationReducer from '../features/navigations/navigationSlice'
+ import postReducer from "../features/posts/postSlice";
export const store = configureStore({
    reducer: {
        navigation: navigationReducer,
+       posts:postReducer
    },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

We are now ready to start saving a new post.

Open up the AddNewPost component and update it with the following changes to save a new post:

import { useState } from "react";
import { IPost } from "../../typings";
import PostInputForm from "./PostInputForm";

+import { useAppDispatch } from "../../app/hooks";
+import { saveBlogPost } from "./postSlice";
+import { switchToAllPostsComponent } from "../navigations/navigationSlice";

const AddNewPost = () => {
  const [title, setTitle] = useState("Blog Title");
  const [content, setContent] = useState("Blog Details");

+  const dispatch = useAppDispatch();

  const submitPost = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const post: IPost = {
      id: Date.now(),
      title,
      content,
      voteCount: 0,
      isFavorite: false,
    };
-    console.log(post);
+   dispatch(saveBlogPost(post));
+   dispatch(switchToAllPostsComponent());
  };

  return (
    <>
      <PostInputForm
        submitPost={submitPost}
        title={title}
        content={content}
        setTitle={setTitle}
        setContent={setContent}
      />
    </>
  );
};
export default AddNewPost;

We imported the useAppDispatch hook, as well as two action creators named saveBlogPost and switchToAllPostsComponent from the “postSlice” and “navigationSlice” files.

When the user submits the form, the submitPost function is called, which prevents the default form submission behavior and creates a new IPost object using the current state of the input fields. The saveBlogPost function from the postSlice slice is then dispatched with the new post as its argument. Finally, the switchToAllPostsComponent function from the navigationSlice slice is dispatched to switch the app’s current view to the list of all posts.

Let’s update the NoPost component to switch to AddNewPost component whenever the “Add new” button is clicked:

+import { useAppDispatch } from "../../app/hooks";
+import { switchToAddNewPostComponent } from "../navigations/navigationSlice";

const NoPosts = () => {
+  const dispatch = useAppDispatch();
  return (
    <>
      <section className="no-posts-card">
        <h1>You have not created any blog posts.</h1>
-        <button>Add New</button>
+        <button onClick={() => dispatch(switchToAddNewPostComponent())}>
+          Add New
+        </button>
      </section>
    </>
  );
};
export default NoPosts;

We have implemented the functionality to save new blog posts, but we are currently unable to view them since the “view all posts” feature has not yet been implemented. In the next section, we will update the AllPosts component to display all the posts.

Fetching all the Posts

To retrieve all posts in the app, we will update the AllPosts component to fetch all the available posts from the store and render them.

Open the AllPosts.tsx and update it with the following code:

import BlogCard from "./BlogCard";
import NoPosts from "./NoPosts";

+import { useAppSelector } from "../../app/hooks";
+import { selectAllPosts } from "./postSlice";

const AllPosts = () => {
-  const allPosts: any = [];
+const {allPosts} = useAppSelector(selectAllPosts);
  if (!allPosts.length) {
    return <NoPosts />;
  }
  return (
    <>
      <BlogCard posts={allPosts} />
    </>
  );
};
export default AllPosts;

First, we import the BlogCard and NoPosts components, as well as the useAppSelector and selectAllPosts functions from the postSlice file.

The component first calls the useAppSelector hook with the selectAllPosts function to get the allPosts array from the Redux store. If the allPosts array is empty, the component renders the NoPosts component. Otherwise, it renders the BlogCard component with the allPosts array passed in as a prop.

The BlogCard component will then render each blog post in the allPosts array as a separate card.

Now we can update the BlogCard component with the following changes to display each post on a card:

+import { IPost } from "../../typings";

-const BlogCard = (props: { posts: any }) => {
+const BlogCard = (props: { posts: IPost[] }) => {
    return (
      <>
-     {props.posts.map((post: any) => {
+        {props.posts.map((post) => {
          return (
            <section className="card" key={post.id}>
              <div className="card-header">
                <span className="edit-icon">✎</span>
                <h1 className="delete-icon">x</h1>
              </div>
              <section className="card-body">
                <h1 className="card-title">
                  {post.title.length > 150
                    ? post.title.slice(0, 150) + "..."
                    : post.title}
                </h1>
                <p className="card-summary">
                  {post.content.length > 120
                    ? post.content.slice(0, 120) + "..."
                    : post.content}
                </p>
              </section>
              <section className="card-bottom">
                <section>
                  <span
                    className="favorite"
                  >
                   </span>
                </section>
                <section className="rating-section">
                  <span className="upvote">⇧</span>
                  <span className="vote-counter">0</span>
                  <span className="downvote">⇩</span>
                </section>
              </section>
            </section>
          );
        })}
      </>
    );
  };

  export default BlogCard;

Upvoting and downvoting a post

We’ll create two reducer functions, namely upvotePost and downvotePost in the postSlice, which will be used for upvoting and downvoting a post, and the resulting action creators will be exported.

Open the postSlice.tsx file and update it with the following changes:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { IPost } from "../../typings";

interface IAllPosts {
  allPosts: IPost[];
}

const initialState: IAllPosts = { allPosts: [] };

export const postSlice = createSlice({
  name: "all-posts",
  initialState: initialState,
  reducers: {
    saveBlogPost: (state, actions: { payload: IPost }) => {
      state.allPosts.push(actions.payload);
    },
+    upvotePost: (state, actions: { payload: number }) => {
+       const updatedPosts = state.allPosts.map((post) => {
+        if (post.id === actions.payload) {
+          return { ...post, voteCount: post.voteCount + 1 };
+        }
+        return post;
+      });
+        state.allPosts = updatedPosts;
+    },
+    downvotePost: (state, actions: { payload: number }) => {
+        const updatedPosts = state.allPosts.map((post) => {
+         if (post.id === actions.payload) {
+           return { ...post, voteCount: post.voteCount - 1 };
+         }
+         return post;
+       });
+         state.allPosts = updatedPosts;
+     },
  },
});

export const selectAllPosts = (state: RootState) => state.posts;

export const {
  saveBlogPost,
+  upvotePost,
+  downvotePost,
} = postSlice.actions;
export default postSlice.reducer;

The upvotePost reducer function takes the current state and a payload containing a post ID and returns a new array where the post with the matching ID has its voteCount property incremented by 1 using the map method. The downvotePost reducer, on the other hand, behaves similarly to the upvotePost reducer, but instead of increasing the voteCount property, it decreases it by 1 for the selected post.

Note that the state.allPosts array was directly modified. It is made possible by createSlice, which allows us to write “mutating” logic in reducers.

We need to modify the BlogCard component so that when a user clicks on the upward arrow icon, the post is upvoted, and when thy click on the downward arrow icon, the post is downvoted.

Open the BlogCard.tsx file and update it with the following changes:

import { IPost } from "../../typings";

+import { downvotePost, upvotePost } from "./postSlice";
+import { useAppDispatch } from "../../app/hooks";
const BlogCard = (props: { posts: IPost[] }) => {
+  const dispatch = useAppDispatch();
  return (
    <>
      {props.posts.map((post) => {
        return (
          <section className="card" key={post.id}>
            <div className="card-header">
              <span className="edit-icon">✎</span>
              <h1 className="delete-icon">x</h1>
            </div>
            <section className="card-body">
              <h1 className="card-title">
                {post.title.length > 150
                  ? post.title.slice(0, 150) + "..."
                  : post.title}
              </h1>
              <p className="card-summary">
                {post.content.length > 120
                  ? post.content.slice(0, 120) + "..."
                  : post.content}
              </p>
            </section>
            <section className="card-bottom">
              <section>
                <span
                  className="favorite"
                >
                 </span>
              </section>
              <section className="rating-section">
-                  <span className="upvote">⇧</span>
-                  <span className="vote-counter">0</span>
-                  <span className="downvote">⇩</span>
+                <span
+                  className="upvote"
+                  onClick={() => dispatch(upvotePost(post.id))}
+                >
++                </span>
+                <span className="vote-counter">{post.voteCount}</span>
+                <span
+                  className="downvote"
+                  onClick={() => dispatch(downvotePost(post.id))}
+                >
++                </span>
              </section>
            </section>
          </section>
        );
      })}
    </>
  );
};

export default BlogCard;

Add and remove a post from favorites

To enable the addition or removal of a post from favorites, we will create a new reducer function called toggleFavorite. We will also export the action creator for this function so that it can be used in other parts of the application as needed.

Go to the postSlice.ts file and update it with the following code:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { IPost } from "../../typings";

interface IAllPosts {
  allPosts: IPost[];
}

const initialState: IAllPosts = { allPosts: [] };

export const postSlice = createSlice({
  name: "all-posts",
  initialState: initialState,
  reducers: {
    saveBlogPost: (state, actions: { payload: IPost }) => {
      state.allPosts.push(actions.payload);
    },
    upvotePost: (state, actions: { payload: number }) => {
       const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount + 1 };
        }
        return post;
      });
        state.allPosts = updatedPosts;
    },
    downvotePost: (state, actions: { payload: number }) => {
        const updatedPosts = state.allPosts.map((post) => {
         if (post.id === actions.payload) {
           return { ...post, voteCount: post.voteCount - 1 };
         }
         return post;
       });
         state.allPosts = updatedPosts;
      },
+      toggleFavorite: (state, actions: { payload: number }) => {
+        const updatedPosts =  state.allPosts.map((post) => {
+          if (post.id === actions.payload) {
+            return { ...post, isFavorite: !post.isFavorite };
+          }
+          return post;
+        });
+        state.allPosts = updatedPosts;
+      },
  },
});

export const selectAllPosts = (state: RootState) => state.posts;

export const {
  saveBlogPost,
  upvotePost,
  downvotePost,
+ toggleFavorite
} = postSlice.actions;

export default postSlice.reducer;

In the code above, we defined a reducer function named toggleFavorite that toggles the isFavorite property of a blog post. The function takes the current state and an action containing the ID of the post to toggle. It creates a new array of updated posts by mapping over the existing posts array and toggling the isFavorite property of the post with the given ID. Finally, the function updates the state’s allPosts property with the new array of updated posts.

Let’s update the BlogCard component to call the toggleFavorite function with the ID of the post anytime a user clicks on the favorite icon on a post.

Open the Blog.ts file and modify it with the following additions:

import { IPost } from "../../typings";

import { downvotePost,
         upvotePost,
+        toggleFavorite
   } from "./postSlice";

import { useAppDispatch } from "../../app/hooks";
const BlogCard = (props: { posts: IPost[] }) => {
  const dispatch = useAppDispatch();
  return (
    <>
      {props.posts.map((post) => {
        return (
          <section className="card" key={post.id}>
            <div className="card-header">
              <span className="edit-icon">✎</span>
              <h1 className="delete-icon">x</h1>
            </div>
            <section className="card-body">
              <h1 className="card-title">
                {post.title.length > 150
                  ? post.title.slice(0, 150) + "..."
                  : post.title}
              </h1>
              <p className="card-summary">
                {post.content.length > 120
                  ? post.content.slice(0, 120) + "..."
                  : post.content}
              </p>
            </section>
            <section className="card-bottom">
-           <section>
+            <section onClick={()=>dispatch(toggleFavorite(post.id))}>
-                <span className="favorite">♥</span>
+             <span className="favorite">{post.isFavorite ? "♥" : "♡"}</span>
              </section>
              <section className="rating-section">
                {/* <span className="upvote">⇧</span>
                  <span className="vote-counter">0</span>
                  <span className="downvote">⇩</span> */}
                <span
                  className="upvote"
                  onClick={() => dispatch(upvotePost(post.id))}
                >
                 </span>
                <span className="vote-counter">{post.voteCount}</span>
                <span
                  className="downvote"
                  onClick={() => dispatch(downvotePost(post.id))}
                >
                 </span>
              </section>
            </section>
          </section>
        );
      })}
    </>
  );
};

export default BlogCard;

Viewing the post details

To display the details of a post, we will create a new object called currentPost. The logic behind viewing the details of a post is simple. Whenever a user clicks on a post, the post will be saved to the currentPost object, and the ViewPostDetails component will be rendered immediately. Once the ViewPostDetails component is rendered, it will retrieve the post stored in the currentPost object and display its title and contents.

Open up the postSlice.ts file and make the following changes to it:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { IPost } from "../../typings";

interface IAllPosts {
  allPosts: IPost[];
+  currentPost: IPost;
}

+const defaultCurrentPost: IPost = {
+  id: 0,
+  title: "",
+  content: "",
+  voteCount: 0,
+  isFavorite: false,
+};

const initialState: IAllPosts = {
  allPosts: [],
+  currentPost: defaultCurrentPost,
};

export const postSlice = createSlice({
  name: "all-posts",
  initialState: initialState,
  reducers: {
    saveBlogPost: (state, actions: { payload: IPost }) => {
      state.allPosts.push(actions.payload);
    },
    upvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount + 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    downvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount - 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    toggleFavorite: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, isFavorite: !post.isFavorite };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
+    setCurrentPost: (state, action: { payload: IPost }) => {
+      state.currentPost = action.payload;
+      return state;
+    },
  },
});

export const selectAllPosts = (state: RootState) => state.posts;

export const {
  saveBlogPost,
  upvotePost,
  downvotePost,
  toggleFavorite,
+  setCurrentPost,
} = postSlice.actions;

export default postSlice.reducer;

Firstly, we added a new property called currentPost to the initialState. Then we defined a reducer function called setCurrentPost that takes in the current state of the store and an action, which has a payload property containing the post selected by a user. The reducer function updates the currentPost property of the state to the post object contained in the action payload.

The reducer function will be used to update the state with the post the user clicked on so it can be displayed in the ViewPostDetails component.

Now we can update the ViewPostDetails component to display the post title and contents stored in the currentPost object once it’s rendered:

+import { useAppSelector } from "../../app/hooks";
+import { selectAllPosts } from "./postSlice";

const ViewPostDetails = () => {
-   const post = { title: "", content: "" };
+    const allPost = useAppSelector(selectAllPosts);
    return (
      <>
        <section className="view-post-container">
-        <h1 className="view-post-title">{post.title}</h1>
-        <p className="view-post-content">{post.content}</p>
+        <h1 className="view-post-title"> {allPost.currentPost.title} </h1>
+          <p className="view-post-content"> {allPost.currentPost.content} </p>
        </section>
      </>
    );
  };
  export default ViewPostDetails;

In the BlogCard component, we will create a function called viewDetails. This function will take in the post that a user clicks on as its parameter. When called, the function will dispatch two actions:

  • setCurrentPost(post): this action sets the currentPost state in the store to the post passed as a parameter to viewDetails.
  • switchToViewPostComponent(): this action sets the currentComponent state to ViewPostComponent.

The viewDetails function will then be invoked whenever a post is clicked on.

Open the BlogCard.ts file and update it with the following additions:

import { IPost } from "../../typings";

import {
  downvotePost,
  upvotePost,
  toggleFavorite,
+  setCurrentPost
} from "./postSlice";
import { useAppDispatch } from "../../app/hooks";

+import { switchToViewPostComponent } from "../navigations/navigationSlice";


const BlogCard = (props: { posts: IPost[] }) => {
  const dispatch = useAppDispatch();

+  const viewDetails = (post: IPost) => {
+    dispatch(setCurrentPost(post));
+    dispatch(switchToViewPostComponent());
+  };
  return (
    <>
      {props.posts.map((post) => {
        return (
          <section className="card" key={post.id}>
            <div className="card-header">
              <span className="edit-icon">✎</span>
              <h1 className="delete-icon">x</h1>
            </div>
            <section
              className="card-body"
+              onClick={() => {
+                viewDetails(post);
+              }}
            >
              <h1 className="card-title">
                {post.title.length > 150
                  ? post.title.slice(0, 150) + "..."
                  : post.title}
              </h1>
              <p className="card-summary">
                {post.content.length > 120
                  ? post.content.slice(0, 120) + "..."
                  : post.content}
              </p>
            </section>
            <section className="card-bottom">
              <section onClick={() => dispatch(toggleFavorite(post.id))}>
                <span className="favorite">{post.isFavorite ? "♥" : "♡"}</span>
              </section>
              <section className="rating-section">
                <span
                  className="upvote"
                  onClick={() => dispatch(upvotePost(post.id))}
                >
                 </span>
                <span className="vote-counter">{post.voteCount}</span>
                <span
                  className="downvote"
                  onClick={() => dispatch(downvotePost(post.id))}
                >
                 </span>
              </section>
            </section>
          </section>
        );
      })}
    </>
  );
};

export default BlogCard;

Modify a post

In order to modify an existing post, a new reducer function called updateBlogPost will be created. Open the postSlice.ts file and update it with the following changes:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { IPost } from "../../typings";

interface IAllPosts {
  allPosts: IPost[];
  currentPost: IPost;
}

const defaultCurrentPost: IPost = {
  id: 0,
  title: "",
  content: "",
  voteCount: 0,
  isFavorite: false,
};

const initialState: IAllPosts = {
  allPosts: [],
  currentPost: defaultCurrentPost,
};

export const postSlice = createSlice({
  name: "all-posts",
  initialState: initialState,
  reducers: {
    saveBlogPost: (state, actions: { payload: IPost }) => {
      state.allPosts.push(actions.payload);
    },
    upvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount + 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    downvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount - 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    toggleFavorite: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, isFavorite: !post.isFavorite };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    setCurrentPost: (state, action: { payload: IPost }) => {
      state.currentPost = action.payload;
      return state;
    },
    updatePost: (
      state,
      actions: { payload: { id: number; content: string; title: string } }
    ) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload.id) {
          return {
            ...post,
            title: actions.payload.title,
            content: actions.payload.content,
          };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
+    updateBlogPost: (
+      state,
+      actions: { payload: { id: number; content: string; title: string } }
+    ) => {
+      const updatedPosts = state.allPosts.map((post) => {
+        if (post.id === actions.payload.id) {
+          return {
+            ...post,
+            title: actions.payload.title,
+            content: actions.payload.content,
+          };
+        }
+        return post;
+      });
+      state.allPosts = updatedPosts;
+    },
  },
});

export const selectAllPosts = (state: RootState) => state.posts;

export const {
  saveBlogPost,
  upvotePost,
  downvotePost,
  toggleFavorite,
  setCurrentPost,
+  updateBlogPost,
} = postSlice.actions;

export default postSlice.reducer;

The updateBlogPost function first maps through all the blog posts in state.allPosts array. If it finds the blog post that matches the id provided in the payload, it creates a new object with the same properties as the original post, but with updated title and content properties, using the values provided in the payload.

If it doesn’t find the post with the matching id, it returns the original post object.

After all the posts have been mapped through, the state.allPosts array is updated to the new array of updatedPosts.

Launch the EditPost.tsx file and update it with the following additions:

import { useState } from "react";
- import { IPost } from "../../typings";
import PostInputForm from "./PostInputForm";

+import { useAppDispatch, useAppSelector } from "../../app/hooks";
+import { switchToAllPostsComponent } from "../navigations/navigationSlice";
+import { updateBlogPost, selectAllPosts } from "./postSlice";

const EditPost = () => {
+  const dispatch = useAppDispatch();
+  const allPost = useAppSelector(selectAllPosts);

+  const [title, setTitle] = useState(allPost.currentPost.title);
+  const [content, setContent] = useState(allPost.currentPost.content);

-  const [title, setTitle] = useState("");
-  const [content, setContent] = useState("");
+  const modifyPost = (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    dispatch(updateBlogPost({ id: allPost.currentPost.id, title, content }));
+    dispatch(switchToAllPostsComponent());
+  };
-   const updatePost = (e: React.FormEvent<HTMLFormElement>) => {
-    e.preventDefault();
-    const post: IPost = {
-      id: Date.now(),
-      title,
-      content,
-      voteCount: 0,
-     isFavorite: false,
-    };
-    console.log(post);
-  };
  return (
    <>
      <PostInputForm
-       submitPost={updatePost}
        submitPost={modifyPost}
        title={title}
        content={content}
        setTitle={setTitle}
        setContent={setContent}
      />
    </>
  );
};
export default EditPost;

We created a modifyPost function to handle the form submission when a user modifies a post. Firstly, it dispatches updateBlogPost action with the updated post title and content. Then, it dispatches switchToAllPostsComponent action to switch to the AllPosts component.

Let’s modify the BlogCard component to enable switching to the EditPost component and rendering the PostInputForm component with the post data when a user clicks on the pencil icon on a post.

Open the BlogCard.tsx file and update it with the following changes:

import { IPost } from "../../typings";

import {
  downvotePost,
  upvotePost,
  toggleFavorite,
  setCurrentPost,
} from "./postSlice";
import { useAppDispatch } from "../../app/hooks";

import {
  switchToViewPostComponent,
+  switchToEditPostComponent,
} from "../navigations/navigationSlice";

const BlogCard = (props: { posts: IPost[] }) => {
  const dispatch = useAppDispatch();

  const viewDetails = (post: IPost) => {
    dispatch(setCurrentPost(post));
    dispatch(switchToViewPostComponent());
  };

+  const editBlog = (post: IPost) => {
+    dispatch(setCurrentPost(post));
+    dispatch(switchToEditPostComponent());
+  };
  return (
    <>
      {props.posts.map((post) => {
        return (
          <section className="card" key={post.id}>
            <div className="card-header">
             <span className="edit-icon"
+                    onClick={() => editBlog(post)}
                 >
               </span>
              <h1 className="delete-icon">x</h1>
            </div>
            <section
              className="card-body"
              onClick={() => {
                viewDetails(post);
              }}
            >
              <h1 className="card-title">
                {post.title.length > 150
                  ? post.title.slice(0, 150) + "..."
                  : post.title}
              </h1>
              <p className="card-summary">
                {post.content.length > 120
                  ? post.content.slice(0, 120) + "..."
                  : post.content}
              </p>
            </section>
            <section className="card-bottom">
              <section onClick={() => dispatch(toggleFavorite(post.id))}>
                <span className="favorite">{post.isFavorite ? "♥" : "♡"}</span>
              </section>
              <section className="rating-section">
                <span
                  className="upvote"
                  onClick={() => dispatch(upvotePost(post.id))}
                >
                 </span>
                <span className="vote-counter">{post.voteCount}</span>
                <span
                  className="downvote"
                  onClick={() => dispatch(downvotePost(post.id))}
                >
                 </span>
              </section>
            </section>
          </section>
        );
      })}
    </>
  );
};

export default BlogCard;

We created a new function called editBlog that takes a post parameter. Inside the function, two actions are dispatched. The first dispatched action is setCurrentPost, which sets the currentPost object to be the post passed as an argument. The second dispatched action is switchToEditPostComponent, which switches the current component to the EditPost component. The editBlog function is invoked once a user clicks on the pencil icon.

Deleting a post

Deleting a post from the state is a straightforward process. All we need to do is obtain its id and dispatch an action to delete it.

Open the postSlice.ts file and update it with the following additions:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { IPost } from "../../typings";

interface IAllPosts {
  allPosts: IPost[];
  currentPost: IPost;
}

const defaultCurrentPost: IPost = {
  id: 0,
  title: "",
  content: "",
  voteCount: 0,
  isFavorite: false,
};

const initialState: IAllPosts = {
  allPosts: [],
  currentPost: defaultCurrentPost,
};

export const postSlice = createSlice({
  name: "all-posts",
  initialState: initialState,
  reducers: {
    saveBlogPost: (state, actions: { payload: IPost }) => {
      state.allPosts.push(actions.payload);
    },
    upvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount + 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    downvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount - 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    toggleFavorite: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, isFavorite: !post.isFavorite };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    setCurrentPost: (state, action: { payload: IPost }) => {
      state.currentPost = action.payload;
      return state;
    },
    updatePost: (
      state,
      actions: { payload: { id: number; content: string; title: string } }
    ) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload.id) {
          return {
            ...post,
            title: actions.payload.title,
            content: actions.payload.content,
          };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    updateBlogPost: (
      state,
      actions: { payload: { id: number; content: string; title: string } }
    ) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload.id) {
          return {
            ...post,
            title: actions.payload.title,
            content: actions.payload.content,
          };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
+    deletePost: (state, actions: { payload: number }) => {
+      const updatedPosts = state.allPosts.filter((post) => {
+        return post.id !== actions.payload;
+      });
+      state.allPosts = updatedPosts;
+    },
  },
});

export const selectAllPosts = (state: RootState) => state.posts;

export const {
  saveBlogPost,
  upvotePost,
  downvotePost,
  toggleFavorite,
  setCurrentPost,
  updateBlogPost,
+  deletePost,
} = postSlice.actions;

export default postSlice.reducer;

We defined a reducer function called deletePost that takes the state and actions parameters. The payload property of the actions parameter is expected to contain the id of the post to be deleted. Inside the function, a new array of updated posts is created by filtering the allPosts array in the state. The filter method returns a new array with all elements that meet a certain condition defined in the callback function. In this case, the condition is that the id of the post should not be equal to the payload value.

Finally, the allPosts property in the state is updated to the new array of updated posts, effectively removing the post with the matching id from the list.

To enable the deletion of a post, the reducer function must be dispatched when the delete icon (x) is clicked. To achieve this, add the following updates to the BlogCard component:

import { IPost } from "../../typings";

import {
  downvotePost,
  upvotePost,
  toggleFavorite,
  setCurrentPost,
+  deletePost,
} from "./postSlice";

import { useAppDispatch } from "../../app/hooks";

import {
  switchToViewPostComponent,
  switchToEditPostComponent,
} from "../navigations/navigationSlice";

const BlogCard = (props: { posts: IPost[] }) => {
  const dispatch = useAppDispatch();

  const viewDetails = (post: IPost) => {
    dispatch(setCurrentPost(post));
    dispatch(switchToViewPostComponent());
  };

  const editBlog = (post: IPost) => {
    dispatch(setCurrentPost(post));
    dispatch(switchToEditPostComponent());
  };
  return (
    <>
      {props.posts.map((post) => {
        return (
          <section className="card" key={post.id}>
            <div className="card-header">
              <span className="edit-icon" onClick={() => editBlog(post)}>
               </span>
-              <h1 className="delete-icon">x</h1>
+              <h1 className="delete-icon"onClick={()=>dispatch(deletePost(post.id))}>x</h1>
            </div>
            <section
              className="card-body"
              onClick={() => {
                viewDetails(post);
              }}
            >
              <h1 className="card-title">
                {post.title.length > 150
                  ? post.title.slice(0, 150) + "..."
                  : post.title}
              </h1>
              <p className="card-summary">
                {post.content.length > 120
                  ? post.content.slice(0, 120) + "..."
                  : post.content}
              </p>
            </section>
            <section className="card-bottom">
              <section onClick={() => dispatch(toggleFavorite(post.id))}>
                <span className="favorite">{post.isFavorite ? "♥" : "♡"}</span>
              </section>
              <section className="rating-section">
                <span
                  className="upvote"
                  onClick={() => dispatch(upvotePost(post.id))}
                >
                 </span>
                <span className="vote-counter">{post.voteCount}</span>
                <span
                  className="downvote"
                  onClick={() => dispatch(downvotePost(post.id))}
                >
                 </span>
              </section>
            </section>
          </section>
        );
      })}
    </>
  );
};

export default BlogCard;

Fetching all favorite posts

To fetch all the favorite posts in the app, we will create a selector function that will be used to retrieve all the posts marked as favorite.

Add the following updates to the postSlice.tsx file:

import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../../app/store";
import { IPost } from "../../typings";

interface IAllPosts {
  allPosts: IPost[];
  currentPost: IPost;
}

const defaultCurrentPost: IPost = {
  id: 0,
  title: "",
  content: "",
  voteCount: 0,
  isFavorite: false,
};

const initialState: IAllPosts = {
  allPosts: [],
  currentPost: defaultCurrentPost,
};

export const postSlice = createSlice({
  name: "all-posts",
  initialState: initialState,
  reducers: {
    saveBlogPost: (state, actions: { payload: IPost }) => {
      state.allPosts.push(actions.payload);
    },
    upvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount + 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    downvotePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, voteCount: post.voteCount - 1 };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    toggleFavorite: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload) {
          return { ...post, isFavorite: !post.isFavorite };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    setCurrentPost: (state, action: { payload: IPost }) => {
      state.currentPost = action.payload;
      return state;
    },
    updatePost: (
      state,
      actions: { payload: { id: number; content: string; title: string } }
    ) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload.id) {
          return {
            ...post,
            title: actions.payload.title,
            content: actions.payload.content,
          };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    updateBlogPost: (
      state,
      actions: { payload: { id: number; content: string; title: string } }
    ) => {
      const updatedPosts = state.allPosts.map((post) => {
        if (post.id === actions.payload.id) {
          return {
            ...post,
            title: actions.payload.title,
            content: actions.payload.content,
          };
        }
        return post;
      });
      state.allPosts = updatedPosts;
    },
    deletePost: (state, actions: { payload: number }) => {
      const updatedPosts = state.allPosts.filter((post) => {
        return post.id !== actions.payload;
      });
      state.allPosts = updatedPosts;
    },
  },
});

export const selectAllPosts = (state: RootState) => state.posts;

+export const selectAllFavoritePosts = (state: RootState) =>
+  state.posts.allPosts.filter((post) => {
+    return post.isFavorite === true;
+  });

export const {
  saveBlogPost,
  upvotePost,
  downvotePost,
  toggleFavorite,
  setCurrentPost,
  updateBlogPost,
  deletePost,
} = postSlice.actions;

export default postSlice.reducer;

We defined a selector function named selectAllFavoritePosts that takes the state as input and returns an array of all the posts in the allPosts array that have their isFavorite property set to true.

The filter function is used to iterate over each element in the allPosts array and returns a new array of only the posts that pass the condition of having isFavorite set to true. The returned array contains only the favorite posts.

Make the following changes to the FavoritePosts component:

import BlogCard from "./BlogCard";
import NoFavoritePosts from "./NoFavoritePosts";

+import { selectAllFavoritePosts } from "./postSlice";
+import { useAppSelector } from "../../app/hooks";

const FavoritePosts = () => {
- const favoritePosts: any[] = [];
+  const favoritePosts = useAppSelector(selectAllFavoritePosts);
  if (!favoritePosts.length) {
    return <NoFavoritePosts />;
  }
  return (
    <>
      <BlogCard posts={favoritePosts} />
    </>
  );
};
export default FavoritePosts;

Now, whenever the FavoritePosts component is rendered, the selectAllFavoritePosts function will be invoked to fetch all the favorite posts, which will then be displayed on the screen.

Next, we’ll update the NoFavoritePosts so that whenever a user clicks on the “Add a post to your favorites” button, the AllPosts component will be rendered:

+import { useAppDispatch } from "../../app/hooks";
+import { switchToAllPostsComponent } from "../navigations/navigationSlice";

const NoFavoritePosts = () => {
+  const dispatch = useAppDispatch();
  return (
    <>
      <section className="no-posts-card">
        <h1>You haven't marked any blog posts as favorites.</h1>
-         <button>Add a post to your favorites</button>
+        <button onClick={() => dispatch(switchToAllPostsComponent())}>
+          Add a post to your favorites
+        </button>
      </section>
    </>
  );
};
export default NoFavoritePosts;

Summary and next steps

In this tutorial, we successfully created the front-end part of a blog application using React and the latest version of Redux Toolkit. The app includs various features such as navigation, post creation, editing and deletion, upvoting and downvoting, and adding and retrieving posts to favorites.

Moreover, we used the createSlice function to write reducer logics that appeared to mutate the state but without actually mutating it. The Immer library was used by the createSlice function to identify changes to a “draft state” and generate a new immutable state based on those changes.

So far, our application has exclusively used synchronous logic. When actions are dispatched, the store runs the reducers, calculates the new state and the dispatch function completes. Nevertheless, it is recommended that you familiarize yourself with Middleware and Side Effects in Redux because the Redux store is unaware of any asynchronous logic. Any asynchronous actions, such as retrieving data from an API or saving a file, must occur outside the store. By learning more about side effects in Redux, you will learn how to write asynchronous logic with Thunk.