Create a Bulletin Board Web App with React & Redux State Management

Welcome to the exciting world of web development with React and Redux! In this blog post, we'll take you on a journey to build a Bulletin Board Web App, a powerful and interactive application that showcases the capabilities of these cutting-edge technologies.

By the end of this tutorial, you'll have the skills and knowledge to create a dynamic bulletin board where users can post, edit, and interact with messages in real-time. Get ready to dive into the world of React and Redux state management as we embark on this creative and informative development adventure. Let's begin building your very own Bulletin Board Web App!


Initial Set Up

Create a new react app using the following command:

npx create-react-app redux-bulletin-board

Open the newly created app in VS Code editor. Remove the boilerplate files and the test files from the source folder, such that we’re only left with App.js, index.js and index.css files.

Next, we will install the redux toolkit with the following command:

npm install @reduxjs/toolkit react-redux

After redux is installed, we will create an ‘app’ folder in the source folder. Within the app folder, we will create a store.js file which will behave as the container for the JavaScript app. It stores the whole state of the app in a mutable object tree.

Implement Redux

Next, we will write some introductory logic in store.js file:

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

export const store = configureStore({
    reducer: {
        // will hold the reducers we will create 
    }
})

Next, we will import the store and wrap our app in redux provider in the index.js file:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { store } from "./app/store";
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

The above code sets up and renders a React application using ReactDOM. Here's an explanation of the code:

  • import { store } from "./app/store";: This line imports a Redux store from a file named store.js or store.jsx. The Redux store is used for managing the application state.
  • import { Provider } from "react-redux";: This line imports the Provider component from the react-redux package. The Provider component is used to provide the Redux store to the entire application.
  • <Provider store={store}>: This component wraps the entire application and provides the Redux store to all the components within it. It takes the store as a prop.

Overall, this code sets up the React application, connects it to the Redux store using the Provider component, and renders the App component into the root element.

Next, we will add some standard styling in index.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
  background: #333;
  color: whitesmoke;
}

body {
  min-height: 100vh;
  font-size: 1.5rem;
  padding: 0 10% 10%;
}

input,
textarea,
button,
select {
  font: inherit;
  margin-bottom: 1em;
}

main {
  max-width: 500px;
  margin: auto;
}

section {
  margin-top: 1em;
}

article {
  margin: 0.5em 0.5em 0.5em 0;
  border: 1px solid whitesmoke;
  border-radius: 10px;
  padding: 1em;
}

h1 {
  font-size: 3.5rem;
}

p {
  font-family: Arial, Helvetica, sans-serif;
  line-height: 1.4;
  font-size: 1.2rem;
  margin: 0.5em 0;
}

form {
  display: flex;
  flex-direction: column;
}

.postCredit {
  font-size: 1rem;
}

.reactionButton {
  margin: 0 0.25em 0 0;
  background: transparent;
  border: none;
  color: whitesmoke;
  font-size: 1rem;
}

Now, we will create a features directory within our source folder. Within this, we will create a posts directory within which we will create a postsSlice.js file with the following logic:

import { createSlice } from "@reduxjs/toolkit";

const initialState = [
  {
    id: "1",
    title: "Learning Redux",
    content: "The best tool for state management",
  },
  {
    id: "2",
    title: "Data Flow in Redux",
    content: "I really need to understand the flow of data.",
  },
];

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {},
});

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

export default postsSlice.reducer;

The above code creates a Redux slice for managing a list of posts. It uses the createSlice function from the @reduxjs/toolkit package.

Let's break down the code:

  1. An initial state is defined as an array of objects representing posts. Each post object has properties such as id, title, and content. This initial state represents the default data in the Redux store for the posts slice.
  2. The createSlice function is invoked with an object containing configuration options for creating the slice. The options include:
    • name: A string value representing the name of the slice, which is set to "posts" in this case.
    • initialState: The initial state of the slice, which is set to the array of posts defined earlier.
    • reducers: An object that defines the reducer functions for this slice. In the provided code, the reducers object is empty, indicating that no specific reducer functions are defined.
  3. The createSlice function returns an object that includes an reducer property. This reducer property represents the reducer function generated by createSlice based on the provided configuration.
  4. The export const selectAllPosts statement exports a selector function named selectAllPosts. This function takes the entire state object as an argument and returns the posts slice from the state. This selector allows other parts of the application to retrieve the posts from the Redux store.
  5. The export default statement exports the generated reducer function from the slice.

In summary, this code sets up a Redux slice for managing a list of posts. It defines an initial state representing the default data and creates a reducer function using createSlice. The slice does not have any specific reducer logic defined, but it can be extended by adding reducer functions to the reducers object in the createSlice configuration. The generated reducer function can be used to update the state of the posts slice in the Redux store.

Next, we will import the posts reducer into the store.js file like this:

import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "../features/posts/postsSlice";

export const store = configureStore({
  reducer: {
    // will hold the reducers we will create
    posts: postsReducer,
  },
});

Next, we will create a PostsList.js component within the features > posts directory and write the following logic in it:

import { useSelector } from "react-redux";
import { selectAllPosts } from "./postsSlice";

const PostsList = () => {
  const posts = useSelector(selectAllPosts);

  const renderedPosts = posts.map((post) => (
    <article key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content.substring(0, 100)}</p>
    </article>
  ));

  return (
    <section>
      <h2>Posts</h2>
      {renderedPosts}
    </section>
  );
};

export default PostsList;

The above code is a React component called PostsList. It utilizes the useSelector hook from react-redux and the selectAllPosts selector from the postsSlice.

Let's break down the code:

  1. The useSelector hook is used to extract data from the Redux store. It takes the selectAllPosts selector function as an argument. This hook automatically subscribes the component to updates from the Redux store whenever the selected data changes. In this case, it retrieves the posts array from the Redux store.
  2. The renderedPosts variable is created by mapping over the posts array. For each post object, an <article> element is rendered with the post's id, title, and a truncated content (limited to the first 100 characters). Each <article> element is assigned a unique key prop using the post's id.
  3. The component returns a <section> element that contains an <h2> heading with the text "Posts" and the renderedPosts array, which represents the list of posts rendered as <article> elements.
  4. Finally, the PostsList component is exported as the default export of the module.

In summary, the PostsList component uses the useSelector hook to retrieve the posts array from the Redux store using the selectAllPosts selector. It then renders a list of posts by mapping over the posts array and displaying the id, title, and truncated content of each post.

Next, we will import this component in App.js file in the following way:

import PostsList from "./features/posts/PostsList";

function App() {
  return (
    <main className="App">
      <PostsList />
    </main>
  );
}

export default App;

Next, we will add an action of post being added to the existing posts within the postsSlice.js in the following way:

import { createSlice } from "@reduxjs/toolkit";

const initialState = [
  {
    id: "1",
    title: "Learning Redux",
    content: "The best tool for state management",
  },
  {
    id: "2",
    title: "Data Flow in Redux",
    content: "I really need to understand the flow of data.",
  },
];

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded(state, action) {
      state.push(action.payload);
    },
  },
});

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

export const { postAdded } = postsSlice.actions;

export default postsSlice.reducer;

The above code continues from the previous code snippet and adds a new action and exports it.

Let's explain the additional code:

  1. Inside the reducers field of the postsSlice configuration object, a new reducer function is defined called postAdded. This reducer function takes the current state and an action as parameters. It modifies the state by pushing the action.payload (which represents a new post object) into the state array.
  2. The selectAllPosts selector function is exported. It takes the entire state object as a parameter and returns the state.posts array, which represents all the posts in the Redux store.
  3. The postAdded action is exported from postsSlice.actions. This allows other parts of the application to import and use the postAdded action creator to dispatch actions that add new posts to the Redux store.

Next, we will create a AddPostForm.js file within the posts directory in features with the following logic:

import { useState } from "react";
import { useDispatch } from "react-redux";
import { nanoid } from "@reduxjs/toolkit";
import { postAdded } from "./postsSlice";

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

  const onTitleChanged = (e) => setTitle(e.target.value);
  const onContentChanged = (e) => setContent(e.target.value);

  const onSavePostClicked = () => {
    if (title && content) {
      dispatch(
        postAdded({
          id: nanoid(),
          title,
          content,
        })
      );

      setTitle("");
      setContent("");
    }
  };

  return (
    <section>
      <h2>Add a New Post</h2>
      <form>
        <label htmlFor="postTitle">Post Title:</label>
        <input
          type="text"
          id="postTitle"
          name="postTitle"
          value={title}
          onChange={onTitleChanged}
        />
        <label htmlFor="postContent">Content:</label>
        <textarea
          name="postContent"
          id="postContent"
          value={content}
          onChange={onContentChanged}
        />
        <button type="button" onClick={onSavePostClicked}>
          Save Post
        </button>
      </form>
    </section>
  );
};

export default AddPostForm;

The above code is a React component that represents a form for adding new posts. It uses Redux for state management and dispatching actions to update the store.

Let's break down the code:

  1. The component imports the necessary dependencies: useState from React for managing local component state, useDispatch from react-redux for accessing the Redux store's dispatch function, nanoid from @reduxjs/toolkit for generating unique IDs for the new posts, and postAdded action from the postsSlice module.
  2. The component defines two state variables, title and content, using the useState hook. These variables hold the current values entered by the user in the form fields.
  3. The component initializes the dispatch function using the useDispatch hook from react-redux.
  4. Two event handler functions, onTitleChanged and onContentChanged, are defined to update the title and content state variables as the user types in the corresponding input fields.
  5. The onSavePostClicked function is responsible for dispatching the postAdded action when the user clicks the "Save Post" button. It checks if both the title and content fields are not empty, creates a new post object with a unique ID generated by nanoid(), and dispatches the postAdded action with the new post object as the payload. After dispatching the action, it resets the title and content fields by setting them to empty strings.
  6. The component renders a form with input fields for the post title and content. The values of the input fields are bound to the title and content state variables, respectively. As the user types in the fields, the onTitleChanged and onContentChanged event handlers are triggered to update the corresponding state variables.
  7. The "Save Post" button triggers the onSavePostClicked function when clicked.
  8. The component is exported as the default export.

In summary, this component provides a form for users to add new posts. When the form is submitted, the postAdded action is dispatched with the new post details, and the form fields are reset.

Next, we will import this form component in App.js in the following way:

import PostsList from "./features/posts/PostsList";
import AddPostForm from "./features/posts/AddPostForm";

function App() {
  return (
    <main className="App">
      <AddPostForm />
      <PostsList />
    </main>
  );
}

export default App;

With this, now we can create a new post using the form and it will appear in the posts list, once we click on save post button.

Next, we will refactor our postsSlice.js file by altering this:

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action) {
        state.push(action.payload);
      },
      prepare(title, content) {
        return {
          payload: {
            id: nanoid(),
            title,
            content,
          },
        };
      },
    },
  },
});

We will also import the nanoid in there. Once this is done, we can simplify the AddPostForm.js file’s save post function like this:

// save post to post list
  const onSavePostClicked = () => {
    if (title && content) {
      dispatch(postAdded(title, content));

      setTitle("");
      setContent("");
    }
  };

Now, we will create a posts directory right within the features directory and create a usersSlice.js file with the following logic:

import { createSlice } from "@reduxjs/toolkit";

const initialState = [
  { id: "0", name: "John Doe" },
  { id: "1", name: "Jane Doe" },
  { id: "2", name: "Jimmy Dane" },
];

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {},
});

export const selectAllUsers = (state) => state.users;

export default usersSlice.reducer;

The above code creates a Redux slice for managing a collection of users. Let's break it down:

  1. The code imports createSlice from @reduxjs/toolkit, which is a utility function for creating Redux slices.
  2. The initialState variable is an array of user objects. Each user object has an id and name property.
  3. The usersSlice is created using the createSlice function. It takes an object as its argument with the following properties:
    • name: The name of the slice, which is "users" in this case.
    • initialState: The initial state of the slice, which is the initialState array defined earlier.
    • reducers: An empty object. This slice doesn't define any custom reducer functions.
  4. The slice exports a selector function named selectAllUsers. This function takes the state as an argument and returns the users array from the state.
  5. The slice exports the reducer function as the default export. The reducer function handles actions dispatched to the "users" slice and updates the state accordingly. Since there are no custom reducer functions defined, the default behavior is used, which means the state remains unchanged when an action is dispatched.

In summary, this code creates a Redux slice for managing a collection of users. It defines an initial state, exports a selector function to retrieve all users from the state, and exports the reducer function as the default export.

Next, we will import this users reducer in the store.js file like this:

import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "../features/posts/postsSlice";
import usersReducer from "../features/users/usersSlice";

export const store = configureStore({
  reducer: {
    // will hold the reducers we will create
    posts: postsReducer,
    users: usersReducer,
  },
});

Now we’ll add a userId to the postsSlice.js file in the following way:

import { createSlice, nanoid } from "@reduxjs/toolkit";

const initialState = [
  {
    id: "1",
    title: "Learning Redux",
    content: "The best tool for state management",
  },
  {
    id: "2",
    title: "Data Flow in Redux",
    content: "I really need to understand the flow of data.",
  },
];

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action) {
        state.push(action.payload);
      },
      prepare(title, content, userId) {
        return {
          payload: {
            id: nanoid(),
            title,
            content,
            userId,
          },
        };
      },
    },
  },
});

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

export const { postAdded } = postsSlice.actions;

export default postsSlice.reducer;

Next, we will add the author selection field in the form in AddPostForm.js file in the following way:

import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { postAdded } from "./postsSlice";
import { selectAllUsers } from "../users/usersSlice";

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

  const users = useSelector(selectAllUsers);
  const dispatch = useDispatch();

  const onTitleChanged = (e) => setTitle(e.target.value);
  const onContentChanged = (e) => setContent(e.target.value);
  const onAuthorChanged = (e) => setUserId(e.target.value);

  // save post to post list
  const onSavePostClicked = () => {
    if (title && content) {
      dispatch(postAdded(title, content, userId));

      setTitle("");
      setContent("");
    }
  };

  const canSave = Boolean(title) && Boolean(content) && Boolean(userId);

  const usersOptions = users.map((user) => (
    <option key={user.id} value={user.id}>
      {user.name}
    </option>
  ));

  return (
    <section>
      <h2>Add a New Post</h2>
      <form>
        <label htmlFor="postTitle">Post Title:</label>
        <input
          type="text"
          id="postTitle"
          name="postTitle"
          value={title}
          onChange={onTitleChanged}
        />
        <label htmlFor="postAuthor">Author: </label>
        <select id="postAuthor" value={userId} onChange={onAuthorChanged}>
          <option value=""></option>
          {usersOptions}
        </select>
        <label htmlFor="postContent">Content:</label>
        <textarea
          name="postContent"
          id="postContent"
          value={content}
          onChange={onContentChanged}
        />
        <button type="button" onClick={onSavePostClicked} disabled={!canSave}>
          Save Post
        </button>
      </form>
    </section>
  );
};

export default AddPostForm;

This code is a React component called AddPostForm that represents a form for adding a new post. Let's break down the code and understand its functionality:

  1. The code imports necessary functions and components from the "react" and "react-redux" libraries, as well as actions and selectors from other files.
  2. The AddPostForm component is defined as a functional component using the arrow function syntax.
  3. Inside the component, three state variables are declared using the useState hook: title, content, and userId. These variables will store the current values of the post's title, content, and the selected user's ID.
  4. The useSelector hook is used to retrieve the array of users from the Redux store, using the selectAllUsers selector function.
  5. The useDispatch hook is used to get the Redux store's dispatch function, which allows dispatching actions to modify the store.
  6. Three functions are defined: onTitleChanged, onContentChanged, and onAuthorChanged. These functions update the respective state variables (title, content, and userId) whenever the user enters or selects new values in the form fields.
  7. The onSavePostClicked function is triggered when the "Save Post" button is clicked. It dispatches the postAdded action from the postsSlice file, passing the title, content, and userId as arguments. After dispatching the action, it resets the title and content state variables to empty strings.
  8. The canSave variable is a boolean value that determines whether the "Save Post" button should be enabled or disabled. It is true only if all the required fields (title, content, and userId) have non-empty values.
  9. The usersOptions variable is an array of JSX <option> elements generated by mapping over the users array. It creates a dropdown list of users where each option represents a user's name and has a corresponding ID value.
  10. The component's JSX code renders a section containing a form with various input fields and a button. The value and onChange attributes of the input fields and the select field are bound to the respective state variables and event handlers.
  11. Finally, the AddPostForm component is exported as the default export of the file, making it available for use in other parts of the application.

In summary, this code provides a form for adding a new post by capturing the post's title, content, and the selected author's ID. When the form is submitted, the post data is dispatched as an action to the Redux store, and the form fields are reset.

Next, we will create a PostAuthor.js file within the posts folder and write the following logic:

import { useSelector } from "react-redux";
import { selectAllUsers } from "../users/usersSlice";

const PostAuthor = ({ userId }) => {
  const users = useSelector(selectAllUsers);

  const author = users.find((user) => user.id === userId);

  return <span>by {author ? author.name : "Unknown Author"}</span>;
};

export default PostAuthor;

This code defines a React component called PostAuthor that displays the name of the author of a post based on the provided userId prop. Let's go through the code to understand its functionality:

  1. The code imports the useSelector hook from the "react-redux" library, as well as the selectAllUsers selector function from the "../users/usersSlice" file.
  2. The PostAuthor component is defined as a functional component that accepts a userId prop.
  3. Inside the component, the useSelector hook is used to retrieve the array of users from the Redux store, using the selectAllUsers selector function.
  4. The find method is called on the users array to find the user object whose id matches the provided userId prop. This will give us the author's information.
  5. The author variable will hold the found user object or undefined if no user with the matching id is found.
  6. The component's JSX code renders a <span> element that displays the name of the author. If an author is found (i.e., author is truthy), it displays the author's name. Otherwise, it displays "Unknown Author".
  7. Finally, the PostAuthor component is exported as the default export of the file, making it available for use in other parts of the application.

In summary, this code provides a component that takes a userId prop and uses it to find the corresponding author's name from the Redux store's user data. It renders the author's name or "Unknown Author" if the author is not found. This component can be used to display the author's name in a post or any other relevant context.

Next, we add this component to the PostsList.js file in the following way:

import { useSelector } from "react-redux";
import { selectAllPosts } from "./postsSlice";
import PostAuthor from "./PostAuthor";

const PostsList = () => {
  const posts = useSelector(selectAllPosts);

  const renderedPosts = posts.map((post) => (
    <article key={post.id}>
      {/* Post */}
      <h3>{post.title}</h3>
      <p>{post.content.substring(0, 100)}</p>

      {/* post Author */}
      <p className="postCredit">
        <PostAuthor userId={post.userId} />
      </p>
    </article>
  ));

  return (
    <section>
      <h2>Posts</h2>
      {renderedPosts}
    </section>
  );
};

export default PostsList;

Next, we will display the date and time when the post was made by installing the date dependency with the npm install date-fns command.

We will add the date properties to the post data and to the prepare as well in postsSlice.js file in the following way:

import { createSlice, nanoid } from "@reduxjs/toolkit";
import { sub } from "date-fns";

const initialState = [
  {
    id: "1",
    title: "Learning Redux",
    content: "The best tool for state management",
    date: sub(new Date(), { minutes: 10 }).toISOString()
  },
  {
    id: "2",
    title: "Data Flow in Redux",
    content: "I really need to understand the flow of data.",
    date: sub(new Date(), { minutes: 5 }).toISOString()
  },
];

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action) {
        state.push(action.payload);
      },
      prepare(title, content, userId) {
        return {
          payload: {
            id: nanoid(),
            title,
            content,
            date: new Date().toISOString(),
            userId,

          },
        };
      },
    },
  },
});

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

export const { postAdded } = postsSlice.actions;

export default postsSlice.reducer;

This code is responsible for creating a Redux slice for managing posts in an application. Let's break it down step by step:

  1. The code imports the createSlice and nanoid functions from the @reduxjs/toolkit library, as well as the sub function from the date-fns library.
  2. An initialState array is defined, which represents the initial state of the posts. It contains two example post objects, each having an id, title, content, and date property.
  3. The createSlice function is called to create a Redux slice for managing posts. It takes an object with several properties:
    • name specifies the name of the slice ("posts").
    • initialState sets the initial state of the slice using the initialState array defined earlier.
    • reducers is an object that contains the reducer functions for handling actions related to posts.
  4. Within the reducers object, there is a single reducer called postAdded. It defines two functions:
    • The first function is the actual reducer function that modifies the state. It appends the action.payload (representing a new post object) to the state array using the push method.
    • The second function, prepare, is a "prepare callback" that returns an object representing the payload for the postAdded action. It takes title, content, and userId as parameters, and constructs a new post object with an auto-generated id using nanoid(), the provided title, content, the current date using new Date().toISOString(), and the userId.
  5. The selectAllPosts function is defined as a selector that retrieves the entire posts state from the Redux store.
  6. The postAdded action is extracted from the postsSlice.actions object and exported.
  7. Finally, the postsSlice.reducer is exported as the default export of the file. This reducer will handle the dispatched actions related to posts and update the state accordingly.

In summary, this code sets up a Redux slice for managing posts. It defines the initial state, a reducer function to handle the postAdded action, and selectors to retrieve the posts state. The prepare function allows the creation of the postAdded action with the necessary payload.

Next we will create a TimeAgo.js file within the posts directory and write the following logic:

import { parseISO, formatDistanceToNow } from "date-fns";

const TimeAgo = ({ timestamp }) => {
  let timeAgo = "";
  if (timestamp) {
    const date = parseISO(timestamp);
    const timePeriod = formatDistanceToNow(date);
    timeAgo = `${timePeriod} ago`;
  }

  return (
    <span title={timestamp}>
      &nbsp; <i>{timeAgo}</i>
    </span>
  );
};

export default TimeAgo;

This code defines a React component called TimeAgo that displays a human-readable representation of the time elapsed since a given timestamp. Let's go through the code to understand its functionality:

  1. The code imports two functions, parseISO and formatDistanceToNow, from the date-fns library. These functions are used to parse and format timestamps.
  2. The TimeAgo component is defined as a functional component that accepts a timestamp prop.
  3. Inside the component, a variable called timeAgo is declared and initialized as an empty string.
  4. The code checks if the timestamp prop is truthy (i.e., if it exists). If so, the code proceeds to calculate the time elapsed since the timestamp.
  5. The parseISO function is used to convert the timestamp string into a valid JavaScript Date object.
  6. The formatDistanceToNow function is then called with the parsed date as an argument. It calculates the difference between the parsed date and the current time and returns a human-readable string representing the distance.
  7. The timePeriod variable holds the human-readable string representing the time elapsed.
  8. The timeAgo variable is set to a formatted string combining the timePeriod and the text "ago".
  9. The component's JSX code renders a <span> element with the title attribute set to the original timestamp value. This attribute provides a tooltip with the full timestamp when the user hovers over the element.
  10. The timeAgo string is displayed inside an <i> element as the content of the <span>. It represents the human-readable time elapsed since the timestamp.
  11. A non-breaking space (&nbsp;) is included before the <i> element to add spacing between the timestamp and other content.
  12. Finally, the TimeAgo component is exported as the default export of the file, making it available for use in other parts of the application.

In summary, this code provides a component that takes a timestamp prop and displays the time elapsed since that timestamp in a human-readable format. It utilizes the parseISO and formatDistanceToNow functions from the date-fns library to perform the necessary conversions and calculations.

Next, we will import this in the PostsList.js file and display it beside the author name:

import { useSelector } from "react-redux";
import { selectAllPosts } from "./postsSlice";
import PostAuthor from "./PostAuthor";
import TimeAgo from "./TimeAgo";

const PostsList = () => {
  const posts = useSelector(selectAllPosts);

  const renderedPosts = posts.map((post) => (
    <article key={post.id}>
      {/* Post */}
      <h3>{post.title}</h3>
      <p>{post.content.substring(0, 100)}</p>

      {/* post Author */}
      <p className="postCredit">
        <PostAuthor userId={post.userId} />
        {/* Time Ago */}
        <TimeAgo timestamp={post.date} />
      </p>
    </article>
  ));

  return (
    <section>
      <h2>Posts</h2>
      {renderedPosts}
    </section>
  );
};

export default PostsList;

We will add a single codeblock that will reverse the order of the posts thereby showing the latest posts first and the oldest ones at the end in PostsList.js file :

import { useSelector } from "react-redux";
import { selectAllPosts } from "./postsSlice";
import PostAuthor from "./PostAuthor";
import TimeAgo from "./TimeAgo";

const PostsList = () => {
  const posts = useSelector(selectAllPosts);

  // to show the latest post first and oldest one in the end
  const orderedPosts = posts
    .slice()
    .sort((a, b) => b.date.localeCompare(a.date));

  const renderedPosts = orderedPosts.map((post) => (
    <article key={post.id}>
      {/* Post */}
      <h3>{post.title}</h3>
      <p>{post.content.substring(0, 100)}</p>

      {/* post Author */}
      <p className="postCredit">
        <PostAuthor userId={post.userId} />
        {/* Time Ago */}
        <TimeAgo timestamp={post.date} />
      </p>
    </article>
  ));

  return (
    <section>
      <h2>Posts</h2>
      {renderedPosts}
    </section>
  );
};

export default PostsList;

The orderedPosts code is used to sort an array of posts (posts) in descending order based on their date property. Let's break it down:

  1. The posts array is accessed, assuming it is defined and contains post objects with a date property.
  2. The slice() method is called on the posts array without any arguments. This creates a shallow copy of the array.
  3. The sort() method is called on the copied array, and a comparator function is provided as an argument. The comparator function determines the order in which the elements are sorted.
  4. The comparator function (a, b) => b.date.localeCompare(a.date) compares two post objects, a and b, based on their date properties.
  5. The localeCompare() method is called on b.date with a.date as the argument. This method compares the two dates as strings in a locale-sensitive way and returns a negative number if b.date comes before a.date, a positive number if b.date comes after a.date, and 0 if they are equal.
  6. The sort() method uses the return value of the comparator function to determine the final order of the elements in the array.
  7. The sorted array is assigned to the orderedPosts variable.

After executing this code, the orderedPosts array will contain the same post objects as the posts array, but they will be sorted in descending order based on their date property. The post with the latest date will be at the beginning of the array, while the post with the oldest date will be at the end.

Next, we will add the ability for a user to add reactions by adding the reactions in postsSlice.js file in the following way:

import { createSlice, nanoid } from "@reduxjs/toolkit";
import { sub } from "date-fns";

const initialState = [
  {
    id: "1",
    title: "Learning Redux",
    content: "The best tool for state management",
    date: sub(new Date(), { minutes: 10 }).toISOString(),
    reactions: {
      thumbsUp: 0,
      wow: 0,
      heart: 0,
      rocket: 0,
      coffee: 0,
    },
  },
  {
    id: "2",
    title: "Data Flow in Redux",
    content: "I really need to understand the flow of data.",
    date: sub(new Date(), { minutes: 5 }).toISOString(),
    reactions: {
      thumbsUp: 0,
      wow: 0,
      heart: 0,
      rocket: 0,
      coffee: 0,
    },
  },
];

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action) {
        state.push(action.payload);
      },
      prepare(title, content, userId) {
        return {
          payload: {
            id: nanoid(),
            title,
            content,
            date: new Date().toISOString(),
            userId,
            reactions: {
              thumbsUp: 0,
              wow: 0,
              heart: 0,
              rocket: 0,
              coffee: 0,
            },
          },
        };
      },
    },
    reactionAdded(state, action) {
      const { postId, reaction } = action.payload;
      const existingPost = state.find((post) => post.id === postId);
      if (existingPost) {
        existingPost.reactions[reaction]++;
      }
    },
  },
});

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

export const { postAdded, reactionAdded } = postsSlice.actions;

export default postsSlice.reducer;

This code extends the previous code example to include an additional action and reducer in the Redux slice for managing posts. Let's break it down:

  1. The initialState array is extended to include a reactions object for each post. The reactions object contains properties representing different types of reactions (e.g., thumbsUp, wow, heart, rocket, coffee), and their initial values are set to 0 for each post.
  2. The postsSlice object is defined using createSlice, and it includes the previous postAdded reducer. Additionally, a new reducer called reactionAdded is added.
  3. The reactionAdded reducer is a standard reducer function that takes the current state and the action as parameters. It extracts the postId and reaction from the action.payload.
  4. The find method is used to search for the post with the matching postId in the state array. If a post is found, the corresponding reactions object's property for the specified reaction is incremented by 1.
  5. Two new named exports, reactionAdded and selectAllPosts, are added to the postsSlice.actions object. This allows other parts of the application to use these action creators and selectors.
  6. Finally, the postsSlice.reducer is exported as the default export of the file, making it available to be combined with other reducers in the Redux store.

In summary, this code extends the Redux slice for managing posts by adding a new action, reactionAdded, and its corresponding reducer. This action is used to increment the count of a specific reaction for a post. The reactions object is added to the initial state and updated in the reactionAdded reducer. This allows for tracking and updating reactions associated with each post.

Next, we will create a reactionButtons.js file in the posts directory with the following logic:

import { useDispatch } from "react-redux";
import { reactionAdded } from "./postsSlice";

const reactionEmoji = {
  thumbsUp: "👍",
  wow: "😮",
  heart: "❤️",
  rocket: "🚀",
  coffee: "🍵",
};

const ReactionButton = ({ post }) => {
  const dispatch = useDispatch();
  const reactionButtons = Object.entries(reactionEmoji).map(([name, emoji]) => {
    return (
      <button
        key={name}
        type="button"
        className="reactionButton"
        onClick={() =>
          dispatch(reactionAdded({ postId: post.id, reaction: name }))
        }
      >
        {emoji} {post.reactions[name]}
      </button>
    );
  });

  return <div>{reactionButtons}</div>;
};

export default ReactionButton;

This code defines a React component called ReactionButton that renders a set of reaction buttons for a post. Let's go through the code to understand its functionality:

  1. The code imports the useDispatch hook from react-redux and the reactionAdded action from the postsSlice file.
  2. An object called reactionEmoji is defined, which maps reaction names to corresponding emojis.
  3. The ReactionButton component is defined as a functional component that accepts a post prop representing the post object.
  4. Inside the component, the useDispatch hook is called to get a reference to the dispatch function from the Redux store.
  5. The reactionButtons variable is initialized by mapping over the entries of the reactionEmoji object using the Object.entries() method. This allows iterating over each entry as an array of [name, emoji].
  6. For each entry, a button element is created. The name and emoji values are used to set the button's text content and the corresponding emoji.
  7. The key prop is set to name to provide a unique identifier for each button.
  8. The className prop is set to "reactionButton" to assign a CSS class to the button.
  9. The onClick event handler is defined, which dispatches the reactionAdded action with an object containing the postId and reaction properties.
  10. The postId is set to post.id to associate the reaction with the specific post.
  11. The reaction is set to name to indicate the type of reaction.
  12. The {emoji} {post.reactions[name]} expression is used to display the emoji and the current count of the corresponding reaction for the post.
  13. The reactionButtons array is rendered within a <div> element.
  14. Finally, the ReactionButton component is exported as the default export of the file, making it available for use in other parts of the application.

In summary, this code provides a component that renders a set of reaction buttons for a post. Each button corresponds to a specific reaction, and when clicked, it dispatches the reactionAdded action with the appropriate postId and reaction values. The component utilizes the useDispatch hook to access the Redux store's dispatch function.

Finally, we will add the reaction buttons to the PostsList.js file in the following way:

import { useSelector } from "react-redux";
import { selectAllPosts } from "./postsSlice";
import PostAuthor from "./PostAuthor";
import TimeAgo from "./TimeAgo";
import ReactionButton from "./ReactionButton";

const PostsList = () => {
  const posts = useSelector(selectAllPosts);

  // to show the latest post first and oldest one in the end
  const orderedPosts = posts
    .slice()
    .sort((a, b) => b.date.localeCompare(a.date));

  const renderedPosts = orderedPosts.map((post) => (
    <article key={post.id}>
      {/* Post */}
      <h3>{post.title}</h3>
      <p>{post.content.substring(0, 100)}</p>

      {/* post Author */}
      <p className="postCredit">
        <PostAuthor userId={post.userId} />
        {/* Time Ago */}
        <TimeAgo timestamp={post.date} />
      </p>
      <ReactionButton post={post} />
    </article>
  ));

  return (
    <section>
      <h2>Posts</h2>
      {renderedPosts}
    </section>
  );
};

export default PostsList;

Async Logic & Thunks in Redux

In order to use the async logic and fetch data using axios, we will first remove the initial state created in postsSlice.js file and replace it with an empty posts array. Once this is done, we will change the state method in everywhere in the file and replace it with state.posts. method to access the posts variable. We will also add a URL from JSON placeholder to fetch the posts. We will install the axios package with npm i axios command and import it in this file.

import { createAsyncThunk, createSlice, nanoid } from "@reduxjs/toolkit";
import { sub } from "date-fns";
import axios from "axios";

// const initialState = [
//   {
//     id: "1",
//     title: "Learning Redux",
//     content: "The best tool for state management",
//     date: sub(new Date(), { minutes: 10 }).toISOString(),
//     reactions: {
//       thumbsUp: 0,
//       wow: 0,
//       heart: 0,
//       rocket: 0,
//       coffee: 0,
//     },
//   },
//   {
//     id: "2",
//     title: "Data Flow in Redux",
//     content: "I really need to understand the flow of data.",
//     date: sub(new Date(), { minutes: 5 }).toISOString(),
//     reactions: {
//       thumbsUp: 0,
//       wow: 0,
//       heart: 0,
//       rocket: 0,
//       coffee: 0,
//     },
//   },
// ];

const POSTS_URL = "<https://jsonplaceholder.typicode.com/posts>";

const initialState = {
  posts: [],
  status: "idle", // 'idle' | 'loading' | 'succeeded' | 'failed'
  error: null,
};

export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => {
  const response = await axios.get(POSTS_URL);
  return response.data;
});

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action) {
        state.posts.push(action.payload);
      },
      prepare(title, content, userId) {
        return {
          payload: {
            id: nanoid(),
            title,
            content,
            date: new Date().toISOString(),
            userId,
            reactions: {
              thumbsUp: 0,
              wow: 0,
              heart: 0,
              rocket: 0,
              coffee: 0,
            },
          },
        };
      },
    },
    reactionAdded(state, action) {
      const { postId, reaction } = action.payload;
      const existingPost = state.posts.find((post) => post.id === postId);
      if (existingPost) {
        existingPost.reactions[reaction]++;
      }
    },
  },
  extraReducers(builder) {
    builder
      .addCase(fetchPosts.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = "succeeded";
        // Adding date and reactions
        let min = 1;
        const loadedPosts = action.payload.map((post) => {
          post.date = sub(new Date(), { minutes: min++ }).toISOString();
          post.reactions = {
            thumbsUp: 0,
            wow: 0,
            heart: 0,
            rocket: 0,
            coffee: 0,
          };
          return post;
        });

        // Add any fetched posts to the array
        state.posts = state.posts.concat(loadedPosts);
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
  },
});

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

export const { postAdded, reactionAdded } = postsSlice.actions;

export default postsSlice.reducer;

This code extends the previous code example to include asynchronous data fetching using Redux Toolkit's createAsyncThunk and handles the loading state, success state, and error state of the API request. Let's break it down:

  1. The code imports createAsyncThunk from Redux Toolkit, axios for making HTTP requests, and the fetchPosts action from the postsSlice file.
  2. The POSTS_URL constant is defined, representing the URL from which posts will be fetched.
  3. The initialState object is modified to include a status property that represents the state of the API request ('idle', 'loading', 'succeeded', 'failed'), and an error property to store any error message related to the API request.
  4. The fetchPosts async thunk is created using createAsyncThunk. It defines an async function that makes a GET request to POSTS_URL using axios.get and returns the response data.
  5. The postsSlice object is updated to include an extraReducers field, which uses the builder pattern to define reducers for handling the pending, fulfilled, and rejected states of the fetchPosts async thunk.
  6. In the pending case, the status property of the state is set to 'loading'.
  7. In the fulfilled case, the status property is set to 'succeeded'. Additionally, for each fetched post, a date is generated using sub from date-fns to simulate the timestamps. The reactions object is added to each post, and the fetched posts are concatenated with the existing posts in the state.
  8. In the rejected case, the status property is set to 'failed', and the error message from the action is stored in the error property of the state.
  9. The selectAllPosts selector is updated to access the posts property in the state.
  10. The postAdded and reactionAdded actions remain unchanged.
  11. The postsSlice.reducer is exported as the default export of the file.

In summary, this code extends the Redux slice for managing posts to include asynchronous data fetching using createAsyncThunk. The fetchPosts thunk performs an API request to fetch posts from a specified URL. The extraReducers field handles the different states of the async thunk, updating the status and error properties of the state accordingly. Fetched posts are added to the state with timestamps and reaction objects.

Next, we will create a PostsExcerpt.js file and transfer the post excerpt from the PostsList.js file to this new file:

import PostAuthor from "./PostAuthor";
import TimeAgo from "./TimeAgo";
import ReactionButton from "./ReactionButton";
const PostsExcerpt = ({ post }) => {
  return (
    <article>
      {/* Post */}
      <h3>{post.title}</h3>
      <p>{post.body.substring(0, 100)}</p>

      {/* post Author */}
      <p className="postCredit">
        <PostAuthor userId={post.userId} />
        {/* Time Ago */}
        <TimeAgo timestamp={post.date} />
      </p>
      <ReactionButton post={post} />
    </article>
  );
};

export default PostsExcerpt;

This code defines the PostsExcerpt component, which represents a summarized view of a post. It renders the post's title, a truncated version of the post's body content, the post's author, the time elapsed since the post was created, and a reaction button.

Let's break down the code:

  1. The component imports the PostAuthor, TimeAgo, and ReactionButton components.
  2. The PostsExcerpt component receives a post prop, which represents the post object to be displayed.
  3. Inside the component's JSX, the post's title is rendered as an h3 element.
  4. The post's body content is rendered as a p element, using the substring method to display only the first 100 characters of the body.
  5. The post's author is rendered by using the PostAuthor component and passing the post.userId as the userId prop.
  6. The time elapsed since the post was created is rendered using the TimeAgo component and passing the post.date as the timestamp prop.
  7. The ReactionButton component is rendered, passing the post object as the post prop.
  8. The entire content is wrapped in an article element.
  9. The PostsExcerpt component is exported as the default export of the file.

In summary, the PostsExcerpt component provides a summarized view of a post by displaying its title, truncated body content, author, elapsed time since creation, and a reaction button.

Next, we will import the above component and make the necessary edits to the PostsList.js file in the following way:

import { useSelector, useDispatch } from "react-redux";
import {
  selectAllPosts,
  getPostsError,
  getPostsStatus,
  fetchPosts,
} from "./postsSlice";
import { useEffect } from "react";
import PostsExcerpt from "./PostsExcerpt";

const PostsList = () => {
  const dispatch = useDispatch();
  const posts = useSelector(selectAllPosts);
  const postStatus = useSelector(getPostsStatus);
  const error = useSelector(getPostsError);

  useEffect(() => {
    if (postStatus === "idle") {
      dispatch(fetchPosts());
    }
  }, [postStatus, dispatch]);

  let content;
  if (postStatus === "loading") {
    content = <p>"Loading..."</p>;
  } else if (postStatus === "succeeded") {
    // to show the latest post first and oldest one in the end
    const orderedPosts = posts
      .slice()
      .sort((a, b) => b.date.localeCompare(a.date));
    content = orderedPosts.map((post) => (
      <PostsExcerpt key={post.id} post={post} />
    ));
  } else if (postStatus === "failed") {
    content = <p>{error}</p>;
  }

  return (
    <section>
      <h2>Posts</h2>
      {content}
    </section>
  );
};

export default PostsList;

This code defines the PostsList component, which displays a list of posts. It uses Redux to manage the state and data fetching.

Let's break down the code:

  1. The component imports the necessary dependencies: useSelector, useDispatch from "react-redux", useEffect from "react", and other functions and components from the postsSlice file.
  2. The PostsList component is defined.
  3. Inside the component, the useDispatch hook is used to get the Redux dispatch function, which allows us to dispatch actions.
  4. The useSelector hook is used to select and extract data from the Redux store. The selectAllPosts, getPostsStatus, and getPostsError functions from the postsSlice file are used as selectors to retrieve the posts array, the status of the posts fetching process, and the error message, respectively.
  5. The useEffect hook is used to fetch the posts data when the component is mounted. It only dispatches the fetchPosts action if the postStatus is "idle", indicating that the data has not been fetched yet.
  6. Based on the postStatus, different content is rendered.
    • If the postStatus is "loading", a loading message is displayed.
    • If the postStatus is "succeeded", the posts are sorted in descending order by date using the sort method, and the sorted posts are mapped to PostsExcerpt components.
    • If the postStatus is "failed", an error message is displayed.
  7. The JSX content is wrapped in a section element, which includes an h2 heading and the content variable that represents the loading, posts, or error message.
  8. The PostsList component is exported as the default export of the file.

In summary, the PostsList component fetches and displays a list of posts. It handles the asynchronous fetching of data, shows loading and error messages, and renders the posts in descending order based on their dates.

Next, we will fetch users from the API to display them as authors of the posts. First, we will edit the logic in usersSlice.js file in the users directory:

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

const USERS_URL = "<https://jsonplaceholder.typicode.com/users>";

const initialState = [];

export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
  const response = await axios.get(USERS_URL);
  return response.data;
});

// const initialState = [
//   { id: "0", name: "John Doe" },
//   { id: "1", name: "Jane Doe" },
//   { id: "2", name: "Jimmy Dane" },
// ];

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {},
  extraReducers(builder) {
    builder.addCase(fetchUsers.fulfilled, (state, action) => {
      return action.payload;
    });
  },
});

export const selectAllUsers = (state) => state.users;

export default usersSlice.reducer;

This code defines a Redux slice for managing user data. It includes an asynchronous thunk action for fetching users from an API and updates the state with the fetched data.

Let's break down the code:

  1. The code imports the necessary dependencies: createSlice and createAsyncThunk from "@reduxjs/toolkit" and axios for making HTTP requests.
  2. The USERS_URL constant is set to the URL of the API endpoint that provides user data.
  3. The initialState variable is set to an empty array, representing the initial state of the users data.
  4. The fetchUsers thunk action is created using createAsyncThunk. It asynchronously fetches the users data from the API using axios.get and returns the response data.
  5. The usersSlice is created using createSlice. It defines the name of the slice as "users" and sets the initial state and reducers. In this case, there are no specific reducers defined.
  6. The extraReducers callback is used to handle the fetchUsers.fulfilled action. When the fetchUsers action is fulfilled (successfully completed), the payload of the action, which contains the fetched users data, is assigned to the state.
  7. The selectAllUsers selector function is defined to select and retrieve the users data from the state.
  8. The usersSlice.reducer is exported as the default export of the file.

In summary, this code defines a Redux slice for managing users data. It provides an asynchronous thunk action for fetching users from an API and updates the state with the fetched data. The users data can be accessed using the selectAllUsers selector.

We will fetch the users when the application loads. So we’ll add it to the index.js file in the following way:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { store } from "./app/store";
import { Provider } from "react-redux";
import { fetchUsers } from "./features/users/usersSlice";

store.dispatch(fetchUsers());

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

And with this, our blog posts will have individual author names sourced from the placeholder API. The author selection list in the form will also have all the author names from the API.

Next, we will add the logic to add a new post in postsSlice.js in the following way:

import { createAsyncThunk, createSlice, nanoid } from "@reduxjs/toolkit";
import { sub } from "date-fns";
import axios from "axios";

const POSTS_URL = "<https://jsonplaceholder.typicode.com/posts>";

const initialState = {
  posts: [],
  status: "idle", // 'idle' | 'loading' | 'succeeded' | 'failed'
  error: null,
};

export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => {
  const response = await axios.get(POSTS_URL);
  console.log(response.data);
  return response.data;
});

export const addNewPost = createAsyncThunk(
  "posts/addNewPost",
  async (initialPost) => {
    const response = await axios.post(POSTS_URL, initialPost);
    return response.data;
  }
);

const postsSlice = createSlice({
  name: "posts",
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action) {
        state.posts.push(action.payload);
      },
      prepare(title, content, userId) {
        return {
          payload: {
            id: nanoid(),
            title,
            content,
            date: new Date().toISOString(),
            userId,
            reactions: {
              thumbsUp: 0,
              wow: 0,
              heart: 0,
              rocket: 0,
              coffee: 0,
            },
          },
        };
      },
    },
    reactionAdded(state, action) {
      const { postId, reaction } = action.payload;
      const existingPost = state.posts.find((post) => post.id === postId);
      if (existingPost) {
        existingPost.reactions[reaction]++;
      }
    },
  },
  extraReducers(builder) {
    builder
      .addCase(fetchPosts.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = "succeeded";
        // Adding date and reactions
        let min = 1;
        const loadedPosts = action.payload.map((post) => {
          post.date = sub(new Date(), { minutes: min++ }).toISOString();
          post.reactions = {
            thumbsUp: 0,
            wow: 0,
            heart: 0,
            rocket: 0,
            coffee: 0,
          };
          return post;
        });

        // Add any fetched posts to the array
        state.posts = state.posts.concat(loadedPosts);
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      })
      .addCase(addNewPost.fulfilled, (state, action) => {
        const sortedPosts = state.posts.sort((a, b) => {
          if (a.id > b.id) return 1;
          if (a.id < b.id) return -1;
          return 0;
        });

        action.payload.id = sortedPosts[sortedPosts.length - 1].id + 1;

        action.payload.userId = Number(action.payload.userId);
        action.payload.date = new Date().toISOString();
        action.payload.reactions = {
          thumbsUp: 0,
          hooray: 0,
          heart: 0,
          rocket: 0,
          eyes: 0,
        };
        console.log(action.payload);
        state.posts.push(action.payload);
      });
  },
});

export const selectAllPosts = (state) => state.posts.posts;
export const getPostsStatus = (state) => state.posts.status;
export const getPostsError = (state) => state.posts.error;

export const { postAdded, reactionAdded } = postsSlice.actions;

export default postsSlice.reducer;

This updated code includes the addition of an asynchronous thunk action addNewPost to create a new post by making a POST request to the API endpoint. It also includes changes in the reducer's extraReducers callback to handle the addNewPost.fulfilled action and add the new post to the state.

Let's go through the changes in the code:

  1. The addNewPost async thunk action is defined using createAsyncThunk. It takes an initialPost parameter representing the data for the new post. Inside the thunk, it sends a POST request to the POSTS_URL with the initialPost data and returns the response data.
  2. In the extraReducers callback, a new case is added to handle the addNewPost.fulfilled action. When the addNewPost action is fulfilled, the payload of the action contains the newly created post data. The code first sorts the existing posts in ascending order based on the id property. Then, it assigns the id of the new post by incrementing the id of the last post in the sorted array. It converts the userId to a number and sets the date to the current date. Additionally, it initializes the reactions for the new post. Finally, the new post is pushed to the state.posts array.
  3. The getPostsStatus and getPostsError selector functions are defined to retrieve the status and error from the state.
  4. The reactionAdded reducer remains unchanged.

In summary, this updated code adds an asynchronous thunk action addNewPost to create a new post and updates the reducer to handle the addNewPost.fulfilled action by adding the new post to the state.

Next, we will edit the logic in AddPostForm.js file in the following way:

import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addNewPost } from "./postsSlice";
import { selectAllUsers } from "../users/usersSlice";

const AddPostForm = () => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [userId, setUserId] = useState("");
  const [addRequestStatus, setAddRequestStatus] = useState("idle");

  const users = useSelector(selectAllUsers);
  const dispatch = useDispatch();

  const onTitleChanged = (e) => setTitle(e.target.value);
  const onContentChanged = (e) => setContent(e.target.value);
  const onAuthorChanged = (e) => setUserId(e.target.value);

  // const canSave = Boolean(title) && Boolean(content) && Boolean(userId);

  const canSave =
    [title, content, userId].every(Boolean) && addRequestStatus === "idle";

  // save post to post list
  // const onSavePostClicked = () => {
  //   if (title && content) {
  //     dispatch(postAdded(title, content, userId));

  //     setTitle("");
  //     setContent("");
  //   }
  // };

  const onSavePostClicked = () => {
    if (canSave) {
      try {
        setAddRequestStatus("pending");
        dispatch(addNewPost({ title, body: content, userId })).unwrap();

        setTitle("");
        setContent("");
      } catch (err) {
        console.error("Failed to save the post", err);
      } finally {
        setAddRequestStatus("idle");
      }
    }
  };

  const usersOptions = users.map((user) => (
    <option key={user.id} value={user.id}>
      {user.name}
    </option>
  ));

  return (
    <section>
      <h2>Add a New Post</h2>
      <form>
        <label htmlFor="postTitle">Post Title:</label>
        <input
          type="text"
          id="postTitle"
          name="postTitle"
          value={title}
          onChange={onTitleChanged}
        />
        <label htmlFor="postAuthor">Author: </label>
        <select id="postAuthor" value={userId} onChange={onAuthorChanged}>
          <option value=""></option>
          {usersOptions}
        </select>
        <label htmlFor="postContent">Content:</label>
        <textarea
          name="postContent"
          id="postContent"
          value={content}
          onChange={onContentChanged}
        />
        <button type="button" onClick={onSavePostClicked} disabled={!canSave}>
          Save Post
        </button>
      </form>
    </section>
  );
};

export default AddPostForm;

The updated code includes changes to the AddPostForm component to handle the creation of new posts using the addNewPost asynchronous thunk action from the postsSlice. Let's go through the changes:

  1. The component now imports the useState hook from React to manage the state of the form inputs and the request status.
  2. The users array is obtained from the Redux store using the selectAllUsers selector.
  3. The dispatch function is retrieved from the react-redux library.
  4. Event handler functions onTitleChanged, onContentChanged, and onAuthorChanged are defined to update the corresponding state variables when the input fields change.
  5. The canSave variable is updated to check if the title, content, and userId fields are non-empty and the addRequestStatus is "idle".
  6. The onSavePostClicked function is updated to dispatch the addNewPost action if the canSave condition is true. The function sets the addRequestStatus to "pending" before dispatching the action. The addNewPost action is awaited using the .unwrap() method to handle any potential errors. After dispatching the action, the title and content are cleared and the addRequestStatus is set back to "idle".
  7. The usersOptions variable is updated to map over the users array and generate <option> elements for each user.
  8. In the JSX markup, the input fields and the select element are updated to use the respective event handlers and state variables. The button's onClick event is updated to call the onSavePostClicked function, and the disabled attribute is set to !canSave to disable the button when the conditions are not met.

Overall, these changes enable the form to create new posts by dispatching the addNewPost action and manage the form state and request status accordingly.

Conclusion

In conclusion, we've explored the remarkable world of web development by creating a Bulletin Board Web App with the powerful combination of React and Redux state management. Throughout this journey, you've learned how to set up a robust application, handle user interactions, and manage the state of your app seamlessly.

By mastering these technologies, you're now equipped to develop a wide range of dynamic web applications and contribute to the ever-evolving field of front-end development. The Bulletin Board Web App is just the beginning, and with the skills you've acquired, the possibilities are endless.

We hope this tutorial has not only empowered you with technical knowledge but also sparked your creativity, enabling you to build web applications that captivate and engage users. The world of web development is at your fingertips, and we can't wait to see what you create next. Happy coding!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript