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.jsorstore.jsx. The Redux store is used for managing the application state.import { Provider } from "react-redux";: This line imports theProvidercomponent from thereact-reduxpackage. TheProvidercomponent 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 thestoreas 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 thepostsslice. - The
createSlicefunction 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, thereducersobject is empty, indicating that no specific reducer functions are defined.
- The
createSlicefunction returns an object that includes anreducerproperty. Thisreducerproperty represents the reducer function generated bycreateSlicebased on the provided configuration. - The
export const selectAllPostsstatement exports a selector function namedselectAllPosts. This function takes the entire state object as an argument and returns thepostsslice from the state. This selector allows other parts of the application to retrieve the posts from the Redux store. - The
export defaultstatement 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
useSelectorhook is used to extract data from the Redux store. It takes theselectAllPostsselector 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 thepostsarray from the Redux store. - The
renderedPostsvariable is created by mapping over thepostsarray. 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 uniquekeyprop using the post'sid. - The component returns a
<section>element that contains an<h2>heading with the text "Posts" and therenderedPostsarray, which represents the list of posts rendered as<article>elements. - Finally, the
PostsListcomponent 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
reducersfield of thepostsSliceconfiguration object, a new reducer function is defined calledpostAdded. This reducer function takes the currentstateand anactionas parameters. It modifies the state by pushing theaction.payload(which represents a new post object) into thestatearray. - The
selectAllPostsselector function is exported. It takes the entirestateobject as a parameter and returns thestate.postsarray, which represents all the posts in the Redux store. - The
postAddedaction is exported frompostsSlice.actions. This allows other parts of the application to import and use thepostAddedaction 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:
useStatefrom React for managing local component state,useDispatchfromreact-reduxfor accessing the Redux store's dispatch function,nanoidfrom@reduxjs/toolkitfor generating unique IDs for the new posts, andpostAddedaction from thepostsSlicemodule. - The component defines two state variables,
titleandcontent, using theuseStatehook. These variables hold the current values entered by the user in the form fields. - The component initializes the
dispatchfunction using theuseDispatchhook fromreact-redux. - Two event handler functions,
onTitleChangedandonContentChanged, are defined to update thetitleandcontentstate variables as the user types in the corresponding input fields. - The
onSavePostClickedfunction is responsible for dispatching thepostAddedaction when the user clicks the "Save Post" button. It checks if both thetitleandcontentfields are not empty, creates a new post object with a unique ID generated bynanoid(), and dispatches thepostAddedaction with the new post object as the payload. After dispatching the action, it resets thetitleandcontentfields 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
titleandcontentstate variables, respectively. As the user types in the fields, theonTitleChangedandonContentChangedevent handlers are triggered to update the corresponding state variables. - The "Save Post" button triggers the
onSavePostClickedfunction 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
createSlicefrom@reduxjs/toolkit, which is a utility function for creating Redux slices. - The
initialStatevariable is an array of user objects. Each user object has anidandnameproperty. - The
usersSliceis created using thecreateSlicefunction. 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 theinitialStatearray 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 theusersarray 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
AddPostFormcomponent is defined as a functional component using the arrow function syntax. - Inside the component, three state variables are declared using the
useStatehook:title,content, anduserId. These variables will store the current values of the post's title, content, and the selected user's ID. - The
useSelectorhook is used to retrieve the array of users from the Redux store, using theselectAllUsersselector function. - The
useDispatchhook is used to get the Redux store'sdispatchfunction, 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
onSavePostClickedfunction is triggered when the "Save Post" button is clicked. It dispatches thepostAddedaction from thepostsSlicefile, passing thetitle,content, anduserIdas arguments. After dispatching the action, it resets thetitleandcontentstate variables to empty strings. - The
canSavevariable is a boolean value that determines whether the "Save Post" button should be enabled or disabled. It istrueonly if all the required fields (title,content, anduserId) have non-empty values. - The
usersOptionsvariable is an array of JSX<option>elements generated by mapping over theusersarray. 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
valueandonChangeattributes of the input fields and the select field are bound to the respective state variables and event handlers. - Finally, the
AddPostFormcomponent 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
useSelectorhook from the "react-redux" library, as well as theselectAllUsersselector function from the "../users/usersSlice" file. - The
PostAuthorcomponent is defined as a functional component that accepts auserIdprop. - Inside the component, the
useSelectorhook is used to retrieve the array of users from the Redux store, using theselectAllUsersselector function. - The
findmethod is called on theusersarray to find the user object whoseidmatches the provideduserIdprop. This will give us the author's information. - The
authorvariable will hold the found user object orundefinedif no user with the matchingidis found. - The component's JSX code renders a
<span>element that displays the name of the author. If an author is found (i.e.,authoris truthy), it displays the author's name. Otherwise, it displays "Unknown Author". - Finally, the
PostAuthorcomponent 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
createSliceandnanoidfunctions from the@reduxjs/toolkitlibrary, as well as thesubfunction from thedate-fnslibrary. - An
initialStatearray is defined, which represents the initial state of the posts. It contains two example post objects, each having anid,title,content, anddateproperty. - The
createSlicefunction is called to create a Redux slice for managing posts. It takes an object with several properties:namespecifies the name of the slice ("posts").initialStatesets the initial state of the slice using theinitialStatearray defined earlier.reducersis an object that contains the reducer functions for handling actions related to posts.
- Within the
reducersobject, 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 thestatearray using thepushmethod. - The second function,
prepare, is a "prepare callback" that returns an object representing the payload for thepostAddedaction. It takestitle,content, anduserIdas parameters, and constructs a new post object with an auto-generatedidusingnanoid(), 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
selectAllPostsfunction is defined as a selector that retrieves the entire posts state from the Redux store. - The
postAddedaction is extracted from thepostsSlice.actionsobject and exported. - Finally, the
postsSlice.reduceris 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,
parseISOandformatDistanceToNow, from thedate-fnslibrary. These functions are used to parse and format timestamps. - The
TimeAgocomponent is defined as a functional component that accepts atimestampprop. - Inside the component, a variable called
timeAgois declared and initialized as an empty string. - The code checks if the
timestampprop is truthy (i.e., if it exists). If so, the code proceeds to calculate the time elapsed since the timestamp. - The
parseISOfunction is used to convert thetimestampstring into a valid JavaScriptDateobject. - The
formatDistanceToNowfunction 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
timePeriodvariable holds the human-readable string representing the time elapsed. - The
timeAgovariable is set to a formatted string combining thetimePeriodand the text "ago". - The component's JSX code renders a
<span>element with thetitleattribute set to the originaltimestampvalue. This attribute provides a tooltip with the full timestamp when the user hovers over the element. - The
timeAgostring 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
TimeAgocomponent 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
postsarray is accessed, assuming it is defined and contains post objects with adateproperty. - The
slice()method is called on thepostsarray 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,aandb, based on theirdateproperties. - The
localeCompare()method is called onb.datewitha.dateas the argument. This method compares the two dates as strings in a locale-sensitive way and returns a negative number ifb.datecomes beforea.date, a positive number ifb.datecomes 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
orderedPostsvariable.
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
initialStatearray is extended to include areactionsobject for each post. Thereactionsobject 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
postsSliceobject is defined usingcreateSlice, and it includes the previouspostAddedreducer. Additionally, a new reducer calledreactionAddedis added. - The
reactionAddedreducer is a standard reducer function that takes the currentstateand theactionas parameters. It extracts thepostIdandreactionfrom theaction.payload. - The
findmethod is used to search for the post with the matchingpostIdin thestatearray. If a post is found, the correspondingreactionsobject's property for the specifiedreactionis incremented by 1. - Two new named exports,
reactionAddedandselectAllPosts, are added to thepostsSlice.actionsobject. This allows other parts of the application to use these action creators and selectors. - Finally, the
postsSlice.reduceris 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
useDispatchhook fromreact-reduxand thereactionAddedaction from thepostsSlicefile. - An object called
reactionEmojiis defined, which maps reaction names to corresponding emojis. - The
ReactionButtoncomponent is defined as a functional component that accepts apostprop representing the post object. - Inside the component, the
useDispatchhook is called to get a reference to the dispatch function from the Redux store. - The
reactionButtonsvariable is initialized by mapping over the entries of thereactionEmojiobject 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
nameandemojivalues are used to set the button's text content and the corresponding emoji. - The
keyprop is set tonameto provide a unique identifier for each button. - The
classNameprop is set to"reactionButton"to assign a CSS class to the button. - The
onClickevent handler is defined, which dispatches thereactionAddedaction with an object containing thepostIdandreactionproperties. - The
postIdis set topost.idto associate the reaction with the specific post. - The
reactionis set tonameto 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
reactionButtonsarray is rendered within a<div>element. - Finally, the
ReactionButtoncomponent 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
createAsyncThunkfrom Redux Toolkit,axiosfor making HTTP requests, and thefetchPostsaction from thepostsSlicefile. - The
POSTS_URLconstant is defined, representing the URL from which posts will be fetched. - The
initialStateobject is modified to include astatusproperty that represents the state of the API request ('idle','loading','succeeded','failed'), and anerrorproperty to store any error message related to the API request. - The
fetchPostsasync thunk is created usingcreateAsyncThunk. It defines an async function that makes a GET request toPOSTS_URLusingaxios.getand returns the response data. - The
postsSliceobject is updated to include anextraReducersfield, which uses thebuilderpattern to define reducers for handling the pending, fulfilled, and rejected states of thefetchPostsasync thunk. - In the
pendingcase, thestatusproperty of the state is set to'loading'. - In the
fulfilledcase, thestatusproperty is set to'succeeded'. Additionally, for each fetched post, a date is generated usingsubfromdate-fnsto simulate the timestamps. Thereactionsobject is added to each post, and the fetched posts are concatenated with the existing posts in the state. - In the
rejectedcase, thestatusproperty is set to'failed', and the error message from the action is stored in theerrorproperty of the state. - The
selectAllPostsselector is updated to access thepostsproperty in the state. - The
postAddedandreactionAddedactions remain unchanged. - The
postsSlice.reduceris 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, andReactionButtoncomponents. - The
PostsExcerptcomponent receives apostprop, which represents the post object to be displayed. - Inside the component's JSX, the post's title is rendered as an
h3element. - The post's body content is rendered as a
pelement, using thesubstringmethod to display only the first 100 characters of the body. - The post's author is rendered by using the
PostAuthorcomponent and passing thepost.userIdas theuserIdprop. - The time elapsed since the post was created is rendered using the
TimeAgocomponent and passing thepost.dateas thetimestampprop. - The
ReactionButtoncomponent is rendered, passing thepostobject as thepostprop. - The entire content is wrapped in an
articleelement. - The
PostsExcerptcomponent 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,useDispatchfrom "react-redux",useEffectfrom "react", and other functions and components from thepostsSlicefile. - The
PostsListcomponent is defined. - Inside the component, the
useDispatchhook is used to get the Reduxdispatchfunction, which allows us to dispatch actions. - The
useSelectorhook is used to select and extract data from the Redux store. TheselectAllPosts,getPostsStatus, andgetPostsErrorfunctions from thepostsSlicefile are used as selectors to retrieve the posts array, the status of the posts fetching process, and the error message, respectively. - The
useEffecthook is used to fetch the posts data when the component is mounted. It only dispatches thefetchPostsaction if thepostStatusis "idle", indicating that the data has not been fetched yet. - Based on the
postStatus, different content is rendered.- If the
postStatusis "loading", a loading message is displayed. - If the
postStatusis "succeeded", the posts are sorted in descending order by date using thesortmethod, and the sorted posts are mapped toPostsExcerptcomponents. - If the
postStatusis "failed", an error message is displayed.
- If the
- The JSX content is wrapped in a
sectionelement, which includes anh2heading and thecontentvariable that represents the loading, posts, or error message. - The
PostsListcomponent 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:
createSliceandcreateAsyncThunkfrom "@reduxjs/toolkit" andaxiosfor making HTTP requests. - The
USERS_URLconstant is set to the URL of the API endpoint that provides user data. - The
initialStatevariable is set to an empty array, representing the initial state of the users data. - The
fetchUsersthunk action is created usingcreateAsyncThunk. It asynchronously fetches the users data from the API usingaxios.getand returns the response data. - The
usersSliceis 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
extraReducerscallback is used to handle thefetchUsers.fulfilledaction. When thefetchUsersaction is fulfilled (successfully completed), the payload of the action, which contains the fetched users data, is assigned to the state. - The
selectAllUsersselector function is defined to select and retrieve the users data from the state. - The
usersSlice.reduceris 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
addNewPostasync thunk action is defined usingcreateAsyncThunk. It takes aninitialPostparameter representing the data for the new post. Inside the thunk, it sends a POST request to thePOSTS_URLwith theinitialPostdata and returns the response data. - In the
extraReducerscallback, a new case is added to handle theaddNewPost.fulfilledaction. When theaddNewPostaction 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 theidproperty. Then, it assigns theidof the new post by incrementing theidof the last post in the sorted array. It converts theuserIdto a number and sets thedateto the current date. Additionally, it initializes the reactions for the new post. Finally, the new post is pushed to thestate.postsarray. - The
getPostsStatusandgetPostsErrorselector functions are defined to retrieve the status and error from the state. - The
reactionAddedreducer 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
useStatehook from React to manage the state of the form inputs and the request status. - The
usersarray is obtained from the Redux store using theselectAllUsersselector. - The
dispatchfunction is retrieved from thereact-reduxlibrary. - Event handler functions
onTitleChanged,onContentChanged, andonAuthorChangedare defined to update the corresponding state variables when the input fields change. - The
canSavevariable is updated to check if thetitle,content, anduserIdfields are non-empty and theaddRequestStatusis"idle". - The
onSavePostClickedfunction is updated to dispatch theaddNewPostaction if thecanSavecondition is true. The function sets theaddRequestStatusto"pending"before dispatching the action. TheaddNewPostaction is awaited using the.unwrap()method to handle any potential errors. After dispatching the action, thetitleandcontentare cleared and theaddRequestStatusis set back to"idle". - The
usersOptionsvariable is updated to map over theusersarray 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
onClickevent is updated to call theonSavePostClickedfunction, and thedisabledattribute is set to!canSaveto 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!
