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 namedstore.js
orstore.jsx
. The Redux store is used for managing the application state.import { Provider } from "react-redux";
: This line imports theProvider
component from thereact-redux
package. TheProvider
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 thestore
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:
- An initial state is defined as an array of objects representing posts. Each post object has properties such as
id
,title
, andcontent
. This initial state represents the default data in the Redux store for theposts
slice. - 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, thereducers
object is empty, indicating that no specific reducer functions are defined.
- The
createSlice
function returns an object that includes anreducer
property. Thisreducer
property represents the reducer function generated bycreateSlice
based on the provided configuration. - The
export const selectAllPosts
statement exports a selector function namedselectAllPosts
. This function takes the entire state object as an argument and returns theposts
slice from the state. This selector allows other parts of the application to retrieve the posts from the Redux store. - 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:
- The
useSelector
hook is used to extract data from the Redux store. It takes theselectAllPosts
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 theposts
array from the Redux store. - The
renderedPosts
variable is created by mapping over theposts
array. For each post object, an<article>
element is rendered with the post'sid
,title
, and a truncatedcontent
(limited to the first 100 characters). Each<article>
element is assigned a uniquekey
prop using the post'sid
. - The component returns a
<section>
element that contains an<h2>
heading with the text "Posts" and therenderedPosts
array, which represents the list of posts rendered as<article>
elements. - 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:
- Inside the
reducers
field of thepostsSlice
configuration object, a new reducer function is defined calledpostAdded
. This reducer function takes the currentstate
and anaction
as parameters. It modifies the state by pushing theaction.payload
(which represents a new post object) into thestate
array. - The
selectAllPosts
selector function is exported. It takes the entirestate
object as a parameter and returns thestate.posts
array, which represents all the posts in the Redux store. - The
postAdded
action is exported frompostsSlice.actions
. This allows other parts of the application to import and use thepostAdded
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:
- The component imports the necessary dependencies:
useState
from React for managing local component state,useDispatch
fromreact-redux
for accessing the Redux store's dispatch function,nanoid
from@reduxjs/toolkit
for generating unique IDs for the new posts, andpostAdded
action from thepostsSlice
module. - The component defines two state variables,
title
andcontent
, using theuseState
hook. These variables hold the current values entered by the user in the form fields. - The component initializes the
dispatch
function using theuseDispatch
hook fromreact-redux
. - Two event handler functions,
onTitleChanged
andonContentChanged
, are defined to update thetitle
andcontent
state variables as the user types in the corresponding input fields. - The
onSavePostClicked
function is responsible for dispatching thepostAdded
action when the user clicks the "Save Post" button. It checks if both thetitle
andcontent
fields are not empty, creates a new post object with a unique ID generated bynanoid()
, and dispatches thepostAdded
action with the new post object as the payload. After dispatching the action, it resets thetitle
andcontent
fields by setting them to empty strings. - 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
andcontent
state variables, respectively. As the user types in the fields, theonTitleChanged
andonContentChanged
event handlers are triggered to update the corresponding state variables. - The "Save Post" button triggers the
onSavePostClicked
function when clicked. - 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:
- The code imports
createSlice
from@reduxjs/toolkit
, which is a utility function for creating Redux slices. - The
initialState
variable is an array of user objects. Each user object has anid
andname
property. - The
usersSlice
is created using thecreateSlice
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 theinitialState
array defined earlier.reducers
: An empty object. This slice doesn't define any custom reducer functions.
- The slice exports a selector function named
selectAllUsers
. This function takes the state as an argument and returns theusers
array from the state. - 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:
- The code imports necessary functions and components from the "react" and "react-redux" libraries, as well as actions and selectors from other files.
- The
AddPostForm
component is defined as a functional component using the arrow function syntax. - Inside the component, three state variables are declared using the
useState
hook:title
,content
, anduserId
. These variables will store the current values of the post's title, content, and the selected user's ID. - The
useSelector
hook is used to retrieve the array of users from the Redux store, using theselectAllUsers
selector function. - The
useDispatch
hook is used to get the Redux store'sdispatch
function, which allows dispatching actions to modify the store. - Three functions are defined:
onTitleChanged
,onContentChanged
, andonAuthorChanged
. These functions update the respective state variables (title
,content
, anduserId
) whenever the user enters or selects new values in the form fields. - The
onSavePostClicked
function is triggered when the "Save Post" button is clicked. It dispatches thepostAdded
action from thepostsSlice
file, passing thetitle
,content
, anduserId
as arguments. After dispatching the action, it resets thetitle
andcontent
state variables to empty strings. - The
canSave
variable is a boolean value that determines whether the "Save Post" button should be enabled or disabled. It istrue
only if all the required fields (title
,content
, anduserId
) have non-empty values. - The
usersOptions
variable is an array of JSX<option>
elements generated by mapping over theusers
array. It creates a dropdown list of users where each option represents a user's name and has a corresponding ID value. - The component's JSX code renders a section containing a form with various input fields and a button. The
value
andonChange
attributes of the input fields and the select field are bound to the respective state variables and event handlers. - 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:
- The code imports the
useSelector
hook from the "react-redux" library, as well as theselectAllUsers
selector function from the "../users/usersSlice" file. - The
PostAuthor
component is defined as a functional component that accepts auserId
prop. - Inside the component, the
useSelector
hook is used to retrieve the array of users from the Redux store, using theselectAllUsers
selector function. - The
find
method is called on theusers
array to find the user object whoseid
matches the provideduserId
prop. This will give us the author's information. - The
author
variable will hold the found user object orundefined
if no user with the matchingid
is found. - 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". - 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:
- The code imports the
createSlice
andnanoid
functions from the@reduxjs/toolkit
library, as well as thesub
function from thedate-fns
library. - An
initialState
array is defined, which represents the initial state of the posts. It contains two example post objects, each having anid
,title
,content
, anddate
property. - 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 theinitialState
array defined earlier.reducers
is an object that contains the reducer functions for handling actions related to posts.
- Within the
reducers
object, there is a single reducer calledpostAdded
. 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 thestate
array using thepush
method. - The second function,
prepare
, is a "prepare callback" that returns an object representing the payload for thepostAdded
action. It takestitle
,content
, anduserId
as parameters, and constructs a new post object with an auto-generatedid
usingnanoid()
, the providedtitle
,content
, the current date usingnew Date().toISOString()
, and theuserId
.
- The first function is the actual reducer function that modifies the state. It appends the
- The
selectAllPosts
function is defined as a selector that retrieves the entire posts state from the Redux store. - The
postAdded
action is extracted from thepostsSlice.actions
object and exported. - 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}>
<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:
- The code imports two functions,
parseISO
andformatDistanceToNow
, from thedate-fns
library. These functions are used to parse and format timestamps. - The
TimeAgo
component is defined as a functional component that accepts atimestamp
prop. - Inside the component, a variable called
timeAgo
is declared and initialized as an empty string. - 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. - The
parseISO
function is used to convert thetimestamp
string into a valid JavaScriptDate
object. - 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. - The
timePeriod
variable holds the human-readable string representing the time elapsed. - The
timeAgo
variable is set to a formatted string combining thetimePeriod
and the text "ago". - The component's JSX code renders a
<span>
element with thetitle
attribute set to the originaltimestamp
value. This attribute provides a tooltip with the full timestamp when the user hovers over the element. - 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. - A non-breaking space (
) is included before the<i>
element to add spacing between the timestamp and other content. - 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:
- The
posts
array is accessed, assuming it is defined and contains post objects with adate
property. - The
slice()
method is called on theposts
array without any arguments. This creates a shallow copy of the array. - 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. - The comparator function
(a, b) => b.date.localeCompare(a.date)
compares two post objects,a
andb
, based on theirdate
properties. - The
localeCompare()
method is called onb.date
witha.date
as the argument. This method compares the two dates as strings in a locale-sensitive way and returns a negative number ifb.date
comes beforea.date
, a positive number ifb.date
comes aftera.date
, and 0 if they are equal. - The
sort()
method uses the return value of the comparator function to determine the final order of the elements in the array. - 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:
- The
initialState
array is extended to include areactions
object for each post. Thereactions
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. - The
postsSlice
object is defined usingcreateSlice
, and it includes the previouspostAdded
reducer. Additionally, a new reducer calledreactionAdded
is added. - The
reactionAdded
reducer is a standard reducer function that takes the currentstate
and theaction
as parameters. It extracts thepostId
andreaction
from theaction.payload
. - The
find
method is used to search for the post with the matchingpostId
in thestate
array. If a post is found, the correspondingreactions
object's property for the specifiedreaction
is incremented by 1. - Two new named exports,
reactionAdded
andselectAllPosts
, are added to thepostsSlice.actions
object. This allows other parts of the application to use these action creators and selectors. - 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:
- The code imports the
useDispatch
hook fromreact-redux
and thereactionAdded
action from thepostsSlice
file. - An object called
reactionEmoji
is defined, which maps reaction names to corresponding emojis. - The
ReactionButton
component is defined as a functional component that accepts apost
prop representing the post object. - Inside the component, the
useDispatch
hook is called to get a reference to the dispatch function from the Redux store. - The
reactionButtons
variable is initialized by mapping over the entries of thereactionEmoji
object using theObject.entries()
method. This allows iterating over each entry as an array of[name, emoji]
. - For each entry, a button element is created. The
name
andemoji
values are used to set the button's text content and the corresponding emoji. - The
key
prop is set toname
to provide a unique identifier for each button. - The
className
prop is set to"reactionButton"
to assign a CSS class to the button. - The
onClick
event handler is defined, which dispatches thereactionAdded
action with an object containing thepostId
andreaction
properties. - The
postId
is set topost.id
to associate the reaction with the specific post. - The
reaction
is set toname
to indicate the type of reaction. - The
{emoji} {post.reactions[name]}
expression is used to display the emoji and the current count of the corresponding reaction for the post. - The
reactionButtons
array is rendered within a<div>
element. - 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:
- The code imports
createAsyncThunk
from Redux Toolkit,axios
for making HTTP requests, and thefetchPosts
action from thepostsSlice
file. - The
POSTS_URL
constant is defined, representing the URL from which posts will be fetched. - The
initialState
object is modified to include astatus
property that represents the state of the API request ('idle'
,'loading'
,'succeeded'
,'failed'
), and anerror
property to store any error message related to the API request. - The
fetchPosts
async thunk is created usingcreateAsyncThunk
. It defines an async function that makes a GET request toPOSTS_URL
usingaxios.get
and returns the response data. - The
postsSlice
object is updated to include anextraReducers
field, which uses thebuilder
pattern to define reducers for handling the pending, fulfilled, and rejected states of thefetchPosts
async thunk. - In the
pending
case, thestatus
property of the state is set to'loading'
. - In the
fulfilled
case, thestatus
property is set to'succeeded'
. Additionally, for each fetched post, a date is generated usingsub
fromdate-fns
to simulate the timestamps. Thereactions
object is added to each post, and the fetched posts are concatenated with the existing posts in the state. - In the
rejected
case, thestatus
property is set to'failed'
, and the error message from the action is stored in theerror
property of the state. - The
selectAllPosts
selector is updated to access theposts
property in the state. - The
postAdded
andreactionAdded
actions remain unchanged. - 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:
- The component imports the
PostAuthor
,TimeAgo
, andReactionButton
components. - The
PostsExcerpt
component receives apost
prop, which represents the post object to be displayed. - Inside the component's JSX, the post's title is rendered as an
h3
element. - The post's body content is rendered as a
p
element, using thesubstring
method to display only the first 100 characters of the body. - The post's author is rendered by using the
PostAuthor
component and passing thepost.userId
as theuserId
prop. - The time elapsed since the post was created is rendered using the
TimeAgo
component and passing thepost.date
as thetimestamp
prop. - The
ReactionButton
component is rendered, passing thepost
object as thepost
prop. - The entire content is wrapped in an
article
element. - 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:
- The component imports the necessary dependencies:
useSelector
,useDispatch
from "react-redux",useEffect
from "react", and other functions and components from thepostsSlice
file. - The
PostsList
component is defined. - Inside the component, the
useDispatch
hook is used to get the Reduxdispatch
function, which allows us to dispatch actions. - The
useSelector
hook is used to select and extract data from the Redux store. TheselectAllPosts
,getPostsStatus
, andgetPostsError
functions from thepostsSlice
file are used as selectors to retrieve the posts array, the status of the posts fetching process, and the error message, respectively. - The
useEffect
hook is used to fetch the posts data when the component is mounted. It only dispatches thefetchPosts
action if thepostStatus
is "idle", indicating that the data has not been fetched yet. - 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 thesort
method, and the sorted posts are mapped toPostsExcerpt
components. - If the
postStatus
is "failed", an error message is displayed.
- If the
- The JSX content is wrapped in a
section
element, which includes anh2
heading and thecontent
variable that represents the loading, posts, or error message. - 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:
- The code imports the necessary dependencies:
createSlice
andcreateAsyncThunk
from "@reduxjs/toolkit" andaxios
for making HTTP requests. - The
USERS_URL
constant is set to the URL of the API endpoint that provides user data. - The
initialState
variable is set to an empty array, representing the initial state of the users data. - The
fetchUsers
thunk action is created usingcreateAsyncThunk
. It asynchronously fetches the users data from the API usingaxios.get
and returns the response data. - The
usersSlice
is created usingcreateSlice
. 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. - The
extraReducers
callback is used to handle thefetchUsers.fulfilled
action. When thefetchUsers
action is fulfilled (successfully completed), the payload of the action, which contains the fetched users data, is assigned to the state. - The
selectAllUsers
selector function is defined to select and retrieve the users data from the state. - 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:
- The
addNewPost
async thunk action is defined usingcreateAsyncThunk
. It takes aninitialPost
parameter representing the data for the new post. Inside the thunk, it sends a POST request to thePOSTS_URL
with theinitialPost
data and returns the response data. - In the
extraReducers
callback, a new case is added to handle theaddNewPost.fulfilled
action. When theaddNewPost
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 theid
property. Then, it assigns theid
of the new post by incrementing theid
of the last post in the sorted array. It converts theuserId
to a number and sets thedate
to the current date. Additionally, it initializes the reactions for the new post. Finally, the new post is pushed to thestate.posts
array. - The
getPostsStatus
andgetPostsError
selector functions are defined to retrieve the status and error from the state. - 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:
- The component now imports the
useState
hook from React to manage the state of the form inputs and the request status. - The
users
array is obtained from the Redux store using theselectAllUsers
selector. - The
dispatch
function is retrieved from thereact-redux
library. - Event handler functions
onTitleChanged
,onContentChanged
, andonAuthorChanged
are defined to update the corresponding state variables when the input fields change. - The
canSave
variable is updated to check if thetitle
,content
, anduserId
fields are non-empty and theaddRequestStatus
is"idle"
. - The
onSavePostClicked
function is updated to dispatch theaddNewPost
action if thecanSave
condition is true. The function sets theaddRequestStatus
to"pending"
before dispatching the action. TheaddNewPost
action is awaited using the.unwrap()
method to handle any potential errors. After dispatching the action, thetitle
andcontent
are cleared and theaddRequestStatus
is set back to"idle"
. - The
usersOptions
variable is updated to map over theusers
array and generate<option>
elements for each user. - 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 theonSavePostClicked
function, and thedisabled
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!