Redux Must Manual, Part 4: Using Redux Data

Redux Must Manual, Part 4: Using Redux Data

Introduction

Part 3: Basic Redux data flow , we learned how to start with an empty Redux + React project setup, add a new state slice, and create React that can read data from the Redux store and dispatch actions to update the data Components. We also studied how data flows in the application, components dispatch actions, reducers process actions and return new state, and components read new state and re-render the UI.

Now that you know the core steps of writing Redux logic, we will use the same steps to add some new features to the social media stream, which will make it even more useful: view a single post, edit an existing post, display the details of the author of the post , Release time stamp and comment button.

information

As a reminder, these code examples focus on the key concepts and changes in each section. For complete changes in the application, see the CodeSandbox project and the project repository

Branch .

View a single post

Since we have the ability to add new posts to the Redux store, we can add more features that use posts data in different ways.

Currently, our post items will be displayed on mainstream pages, but if the text is too long, we will only display content excerpts, and then view the detailed content on our own page corresponding to the post.

Create a standalone Post page

1. we need to add a new one in our posts function folder

SinglePostPage
Components. When the page URL looks similar to
/posts/123
Time, we will use React Router to display this component, where
123
The part should be the ID of the post we want to display.

Import React from 'REACT' Import {useSelector} from 'REACT-Redux' export const SinglePostPage = ( {match} ) => { const {postId} = match.params const post = useSelector( state => state.posts.find( post => post.id === postId) ) if (!post) { return ( < section > < h2 > Post not found! </h2 > </section > ) } return ( < section > < article className = "post" > < h2 > {post.title} </h2 > < p className = "post-content" > {post.content} </p > </article > </section > ) } Copy code

React Router will pass in one

match
The object is used as a prop, which contains the URL information we want to find. When we set up the route to render this component, we will tell it to parse the second part of the URL into a name
postId
Variables and can be changed from
match.params
Read the value in.

Once you have the

postId
Value, you can use it in the selector function to find the correct post object from the Redux store. we know
state.posts
Should be an array of all post objects, so we can use
Array.find()
The function traverses the array and returns the post entry with the required ID.

It s important to note that as long as

useSelector
Change the returned value to the new reference, and the component will re-render.The component should always try to select the minimum amount of data required from the store, which will help ensure that the data is only rendered when it is actually needed.

There may not be a matching post entry in the Store-it may be that the user tried to type the URL directly, or we did not load the correct data. If this happens, then

find()
Function will return
undefined
Instead of the actual post object. Our component needs to check and deal with it by displaying "Post not found!" in the page.

Assuming that there is indeed the correct post object in the store,

useSelector
This object will be returned, and we can use it to render the title and content of the post on the page.

You may notice that this looks like

<PostsList>
The logic in the body of the component is very similar. In this logic, we traverse the entire posts array to display the main summary of the posts list. We can try to extract the Post component that can be used in two places, but there are already some differences in the way the post summary and the entire post are displayed. Even if there is some repetition, it is usually better to write separately for a period of time, and then decide whether the different parts of the code are similar enough in the future, so that we can really extract reusable components.

Add routing to this independent Post page

Now we have a

<SinglePostPage>
Component, we can define a route to display it, and add a link to each post in the homepage feed.

We will

App.js
Import in
SinglePostPage
And add the following route:

import {PostsList} from './features/posts/PostsList' import {AddPostForm} from './features/posts/AddPostForm' import {SinglePostPage} from './features/posts/SinglePostPage' function App () { return ( < Router > < Navbar/> < div className = "App" > < Switch > < Route exact path = "/" render = {() => ( < React.Fragment > < AddPostForm/> < PostsList/> </React.Fragment > )} /> < Route exact path = "/posts/:postId" component = {SinglePostPage}/> < Redirect to = "/"/> </Switch > </div > </Router > ) } Copy code

Then, in

<PostsList>
, We will update the list rendering logic to wrap a route to that specific post
<Link>
:

Import React from 'REACT' Import {useSelector} from 'REACT-Redux' Import {Link} from 'REACT-Router-DOM' export const PostsList = () => { const posts = useSelector( state => state.posts) const renderedPosts = posts.map( post => ( < article className = "post-excerpt" key = {post.id} > < h3 > {post.title} </h3 > < p className = "post-content" > {post.content.substring(0, 100)} </p > < Link to = { `/posts/${ post.id }`} className = "button muted-button" > View Post </Link > </article > )) return ( < section className = "posts-list" > < h2 > Posts </h2 > {renderedPosts} </section > ) } Copy code

And, since we can now click to go to another page, in

<Navbar>
A link to the main posts page in the component will also help:

Import React from 'REACT' Import {Link} from 'REACT-Router-DOM' export const Navbar = () => { return ( < nav > < section > < h1 > Redux Essentials Example </h1 > < div className = "navContent" > < div className = "navLinks" > < Link to = "/" > Posts </Link > </div > </div > </section > </nav > ) } Copy code

Edit post

As a user, it is really annoying to complete a post and save it until you realize that you made a mistake somewhere in the post. It is very useful to have the function of editing the post after creating the post.

Let's add a new

<EditPostForm>
This component can obtain the ID of an existing post, read the post from the store, let the user edit the title and post content, and then save the changes to update the post in the store.

Update post entry

1. we need to update our

postsSlice
To create a new reducer function and an action so that the store knows how to update posts.

in

createSlice()
Inside the call, we should add a new function to
reducers
Object. Remember, the name of the reducer should describe what happened, because whenever this action is dispatched, we will see the reducer name displayed as part of the action type string in Redux DevTools. Our first reducer is called
postAdded
, So we call this newly added
postUpdated
.

In order to update the post object, we need to know:

  • The ID of the post being updated so that we can find the appropriate post object in the state
  • User-entered new
    title
    with
    content
    Field

The Redux action object must have

type
The field, which is usually a descriptive string, and may also contain other fields that contain more information about the event that occurred. By convention, we usually put additional information in a file called
action.payload
In the field, but it s up to us to decide
payload
The content of the field-it can be a string, number, object, array or other content. In this case, since we need three pieces of information, let s plan to
payload
The field as an object contains three fields. This means that the action object will be similar to
{type:'posts/postUpdated', payload: {id, title, content}}
.

By default,

createSlice
The generated action creator expects you to pass in a parameter, and the value will be
action.payload
Put in the action object. Therefore, we can pass objects containing these fields as parameters to
postUpdated
action creator.

We also know that the reducer is responsible for determining how the state should be updated when the action is dispatched. In view of this, we should let the reducer find the correct post object based on the ID, and update the title and content fields in the post specifically.

Finally, we need to export

createSlice
The action creator function generated for us so that the UI can schedule a new one when the user saves the post
postUpdated
action.

With all these requirements in mind, after completion, this is our

postsSlice
definition:

const postsSlice = createSlice({ name : 'posts' , initialState, reducers : { postAdded ( state, action ) { state.push(action.payload) }, postUpdated ( state, action ) { const {id, title, content} = action.payload const existingPost = state.find( post => post.id === id) if (existingPost) { existingPost.title = title existingPost.content = content } } } }) export const {postAdded, postUpdated} = postsSlice.actions Export default postsSlice.reducer copy the code

Create an edit post form

Our new

<EditPostForm>
The component looks the same as
<AddPostForm>
Similar, but the logic needs to be different. We need to retrieve the correct post object from the store, and then use the object to initialize the state field in the component so that the user can make changes. After the user completes the operation, we will save the changed title and content value back to the store. We will also use React Router's history API to switch to a single post page and display the post.

Import React, useState {} from 'REACT' Import {useDispatch, useSelector} from 'REACT-Redux' Import {useHistory} from 'REACT-Router-DOM' import {postUpdated} from './postsSlice' export const EditPostForm = ( {match} ) => { const {postId} = match.params const post = useSelector( state => state.posts.find( post => post.id === postId) ) const [title, setTitle] = useState(post.title) const [content, setContent] = useState(post.content) const dispatch = useDispatch() const history = useHistory() const onTitleChanged = e => setTitle(e.target.value) const onContentChanged = e => setContent(e.target.value) const onSavePostClicked = () => { if (title && content) { dispatch(postUpdated({ id : postId, title, content })) history.push( `/posts/${postId} ` ) } } return ( < section > < h2 > Edit Post </h2 > < form > < label htmlFor = "postTitle" > Post Title: </label > < input type = "text" id = "postTitle" name = "postTitle" placeholder = "What's on your mind?" value = {title} onChange = {onTitleChanged} /> < label htmlFor = "postContent" > Content: </label > < textarea id = "postContent" name = "postContent" value = {content} onChange = {onContentChanged} /> </form > < button type = "button" onClick = { onSavePostClicked} > Save Post </button > </section > ) } Copy code

versus

SinglePostPage
Same, we need to import it
App.js
And add a route showing this component. We should also report to our
SinglePostPage
Add a new link that will be routed to
EditPostPage
,E.g:

import {Link} from 'REACT-Router-DOM' export const SinglePostPage = ( {match} ) => { //omit other contents < p className = "post-content" > {post.content} </p > < Link to = { `/editPost/${ post.id }`} className = "button" > Edit Post </ Link > Copy code

Prepare the action payload

We just saw

createSlice
The action creator usually has one parameter, namely
action.payload
. This is the most common usage pattern for simplification, but sometimes we need to do more work to prepare the content of the action object. for
postAdded
Action, we need to generate a unique ID for the new post, and also need to make sure that the payload is a look like
{id, title, content}
Object.

Now, we are generating the ID and creating the payloa object in the React component and passing the payloa object to

postAdded
in. But, what if we need to dispatch the same action from different components, or the logic of preparing payloa is complicated? Every time we want to dispatch an action, we have to repeat the logic, and we force the component to know exactly what the payloa of the action should look like.

caveat

If the action needs to contain a unique ID or other random value, always generate the ID first and put it in the action object. The Reducer should never calculate random values , because this will make the results unpredictable.

If we write manually

postAdded
In the action creator, you can put the setting logic itself into it:

//hand-written action creator function postAdded ( title, content ) { const id = nanoid() return { type : 'posts/postAdded' , payload : {id, title, content} } } Copy code

However, the Redux Toolkit

createSlice
These action creators are being generated for us. This makes the code shorter because we don t have to write the code ourselves, but we still need a way to customize
action.payload
Content.

Fortunately, when we write a reducer,

createSlice
Allow us to define a "prepare callback" function. The "prepare callback" function can accept multiple parameters, generate random values such as unique IDs, and run any other synchronization logic needed to determine which values to put into the action object. Then it should return an internal containing
payload
The object of the field. (The returned object may also contain a
meta
Field and one
error
Field, the
meta
Fields can be used to add additional descriptive values to the action, and
error
The field should be a Boolean value, indicating whether this action represents some kind of error. )

in

createSlice
of
reducers
Within the field, we can define one of the fields as one that looks like
{reducer, prepare}
Object:

const postsSlice = createSlice({ name : 'posts' , initialState, reducers : { postAdded : { reducer ( state, action ) { state.push(action.payload) }, prepare ( title, content ) { return { payload : { id : nanoid(), title, content } } } } //other reducers here } }) Copy code

Now, our components don't have to worry about what the payload object looks like-the action creator will be responsible for putting it together in the right way. Therefore, we can update the component so that in the dispatch

postAdded
Time will
title
with
content
Pass as a parameter:

const onSavePostClicked = () => { if (title && content) { dispatch(postAdded(title, content)) setTitle( '' ) setContent( '' ) } } Copy code

Users and posts

So far, we only have one state. The logic is

postsSlice.js
Defined in, the data is stored in
state.posts
In, and all our components are related to the posts function. The actual application may have many different state slices, as well as several different "function folders" for Redux logic and React components.

If no one else is involved, then your "social media" application is meaningless. Let's add a feature to track the list of users in our application and update the post-related features to take advantage of this data.

Add a users slice

Since the concept of "users" is different from the concept of "posts", we want to separate the code and data of users from the code and data of posts. We will add a new

features/users
Folder and place a
usersSlice
file. Like posts slice, now we will add some initial entries so that we can use the data.

import {createSlice} from '@reduxjs/toolkit' const initialState = [ { id : '0' , name : 'Tianna Jenkins' }, { id : '1' , name : 'Kevin Grant' }, { id : '2' , name : 'Madison Price' } ] const usersSlice = createSlice({ name : 'users' , initialState, reducers : {} }) Export default usersSlice.reducer copy the code

Currently, we don t need to actually update the data, so we will

reducers
The field is left as an empty object. (We will discuss it again in a later section.)

As before, we will

usersReducer
Import into our store file and add it to the store settings:

import {configureStore} from '@reduxjs/toolkit' import postsReducer from ' ../features/posts/postsSlice ' import usersReducer from ' ../features/users/usersSlice ' export default configureStore({ reducer : { posts : postsReducer, users : usersReducer } }) Copy code

Add the author of the post

Every post in our application is written by one of our users. Every time a new post is added, we should keep track of which user wrote the post. In a real application, we will have some kind of

state.currentUser
Field to track the currently logged in user and use this information when they add a post.

To make this example simpler, we will update

<AddPostForm>
Component so that we can select a user from the drop-down list and include the user's ID in the post. Once our post object contains the user s ID, we can use that ID to find the user s name and display it in each individual post in the UI.

1. we need to update

postAdded
The action creator takes the ID of the user as a parameter and includes it in the action. (We will also be
initialState
Update the existing post entry to have one of the IDs of the sample user
post.user
Field. )

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, user : userId } } } } //other reducers } }) Copy code

Now in our

<AddPostForm>
In, we can use
useSelector
Read the users list from the store and display it as a drop-down list. Then, we will get the ID of the selected user and pass it to
postAdded
action creator. During this process, we can add some validation logic to the form, so that the user can click the "Save Post" button only if some actual text is included in the title and content input:

Features///Posts/AddPostForm.js Import React, useState {} from 'REACT' Import {useDispatch, useSelector} from 'REACT-Redux' import {postAdded} from './postsSlice' export const AddPostForm = () => { const [title, setTitle] = useState( '' ) const [content, setContent] = useState( '' ) const [userId, setUserId] = useState( '' ) const dispatch = useDispatch() const users = useSelector( state => state.users) const onTitleChanged = e => setTitle(e.target.value) const onContentChanged = e => setContent(e.target.value) const onAuthorChanged = e => setUserId(e.target.value) 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 " placeholder = "What's on your mind?" 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 id = "postContent" name = "postContent" value = {content} onChange = {onContentChanged} /> < button type = "button" onClick = {onSavePostClicked} disabled = {!canSave} > Save Post </button > </form > </section > ) } Copy code

Now we need a kind of list item in the posts and

<SinglePostPage>
The method of displaying the name of the author of the post in. Since we want to display the same information in multiple places, we can make a
PostAuthor
Component, the component uses the user ID as props, finds the correct user object, and formats the user name:

Features///Posts/PostAuthor.js Import React from 'REACT' Import {useSelector} from 'REACT-Redux' export const PostAuthor = ( {userId} ) => { const author = useSelector( state => state.users.find( user => user.id === userId) ) return < span > by {author? author.name:'Unknown author'} </span > } Copy code

Note that we follow the same pattern in every component. Any component that needs to read data from the Redux store can be used

useSelector
hook, and extract the specific data it needs. Similarly, many components can access the same data in the Redux store at the same time.

Now we can add

PostAuthor
Import components into
PostsList.js
with
SinglePostPage.js
And render it as
<PostAuthor userId={post.user}/>
, And each time a post entry is added, the name of the selected user should be displayed in the post.

More post features

At this point, we can create and edit posts. Let's add some other logic to make our posts feed more useful.

Date the posts are stored

Social media feeds are usually sorted by the time when the post was created, and show us the time when the post was created in the form of a relative description, such as "5 hours ago". To do this, we need to start tracking the date field of the post entry.

versus

post.user
Field is the same, we will update
postAdded
The prepare callback to ensure that the dispatch action is always included
post.date
. However, this is not another parameter to be passed in. We want to always use the exact timestamp of the dispatch action, so we let the prepare callback itself handle it.

caveat

Redux actions and state should only contain ordinary JS values, such as objects, arrays, and value types. Do not put class instances, functions or other non-serializable values into Redux!.

Because we can't

Date
The class instance is placed in the Redux store, so we will
post.date
The value is tracked as a timestamp string:

//features/posts/postsSlice.js postAdded : { reducer ( state, action ) { state.push(action.payload) }, prepare ( title, content, userId ) { return { payload : { id : nanoid(), date : new Date ().toISOString(), title, content, user : userId, }, } }, }, Copy code

Like the post author, we need to

<PostsList>
with
<SinglePostPage>
The relative timestamp description is displayed in the component. We will add a
<TimeAgo>
Component to handle the relative description of formatted timestamp strings. Such as
date-fns
Libraries like this have some useful utility functions for parsing and formatting dates, we can use them here:

Features///Posts/TimeAgo.js Import React from 'REACT' Import {parseISO, formatDistanceToNow} from 'DATE-FNS' export 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 > ) } Copy code

Sort the list of posts

Currently, our

<PostsList>
Display all posts in the order in which they are saved in the Redux store. Our example first shows the oldest post, and every time a new post is added, it is added to the end of the posts array. This means that the latest post is always at the bottom of the page.

Usually, social media feeds display the latest posts first, and then scroll down to view older posts. Even if the data remains the oldest in the store, we can still

<PostsList>
The data is reordered in the component so that the latest news takes precedence. In theory, because we know
state.posts
The array is already sorted, so we can reverse the list. However, it is better to sort by yourself to make sure.

due to

array.sort()
Will change the existing array, so we need to make
state.posts
And sort the copy. We know our
post.date
The fields are reserved as date and timestamp strings, and we can directly compare them to sort the posts in the correct order:

//features/posts/PostsList.js //Sort posts in reverse chronological order by datetime string const orderedPosts = posts.slice().sort( ( a, b ) => b.date.localeCompare(a.date)) const renderedPosts = orderedPosts.map( post => { return ( < article className = "post-excerpt" key = {post.id} > < h3 > {post.title} </h3 > < div > < PostAuthor userId = { post.user}/> < TimeAgo timestamp = {post.date}/> </div > < p className = "post-content" >{post.content.substring(0, 100)} </p > < Link to = { `/posts/${ post.id }`} className = "button muted-button" > View Post </Link > </article > ) }) Copy code

We also need to

date
Field added to
postsSlice.js
middle
initialState
in. We will use here again
date-fns
Subtract the minutes from the current date/time so that they are different from each other.

//features/posts/postsSlice.js import {createSlice, nanoid} from '@ reduxjs/Toolkit' Import {Sub} from 'DATE-FNS' const initialState = [ { //omitted fields content : 'Hello!' , date : sub( new Date (), { minutes : 10 }).toISOString() }, { //omitted fields content : 'More text' , date : sub( new Date (), { minutes : 5 }).toISOString() } ] Copy code

Post evaluation button

We have added another new feature for this section. Now, our posts are a bit boring. We need to make them more excited, what better way than let our friends add evaluation emojis in our posts?

We will

<PostsList>
with
<SinglePostPage>
Add a row of emoji review buttons at the bottom of each post in. Every time a user clicks one of the evaluation buttons, we need to update the matching counter field for the post in the Redux store. Since the rating counter data is located in the Redux store, switching between different parts of the application should always display the same value in any component that uses the data.

Like the post author and timestamp, we want to use it wherever the post is displayed, so we will create a post with the post as a prop

<ReactionButtons>
Components. We first display the internal buttons and the current evaluation count of each button:

Features///Posts/ReactionButtons.js Import React from 'REACT' const reactionEmoji = { thumbsup : ' ' , Hooray : ' ' , Heart : ' ' , Rocket : ' ' , Eyes : ' ' } export const ReactionButtons = ( {post} ) => { const reactionButtons = Object .entries(reactionEmoji).map( ( [name, emoji] ) => { return ( < button key = {name} type = "button" className = "muted-button reaction-button" > {emoji} {post.reactions[name]} </button > ) }) return < div > {reactionButtons} </div > } Copy code

There is not yet in our data

post.reactions
Field, so we need to update
initialState
post object and
postAdded
Prepare callback function to ensure that each post has the data, such as evaluation:
reactions: {thumbsUp: 0, hooray: 0}
.

Now, we can define a new reducer, and when the user clicks the "evaluation" button, it will handle updating the evaluation count of the post.

Like editing a post, we need to know the ID of the post and which review button the user clicked. We will have a

action.payload
Is a look like
{id, reaction}
Object. Then, the reducer can find the correct post object and update the correct evaluation field.

const postsSlice = createSlice({ name : 'posts' , initialState, reducers : { reactionAdded ( state, action ) { const {postId, reaction} = action.payload const existingPost = state.find( post => post.id === postId) if (existingPost) { existingPost.reactions[reaction]++ } } //other reducers } }) Export const {postAdded, postUpdated, reactionAdded} = postsSlice.actions copy the code

As we have already seen,

createSlice
Allow us to write "change" logic in the reducer. If we don't use
createSlice
And Immer library, then
existingPost.reactions[reaction]++
This line will indeed change the existing
post.reactions
Object, but this may cause errors elsewhere in the application because we did not follow the reducer rules. However, since we are using
createSlice
, So we can write this complex update logic in a simpler way, and let Immer transform this code into a safe and unchangeable execution.

Please note that our action object only contains the minimum information needed to describe what happened. We know which post we need to update and which review name we clicked. We can calculate the new evaluation counter value and put it into the action, but it is always best to make the action object as small as possible and perform the state update calculation in the reducer. This also means that the reducer can contain as much logic as necessary to calculate the new state.

information

When using Immer, you can "change" an existing state object, or you can return a new state value yourself, but you can't return both at the same time. For more details, see the Immer documentation guide on traps and returning new data .

Our last step is to update

<ReactionButtons>
Component to dispatch when the user clicks the button
responseAdded
action:

Features///Posts/ReactionButtons.jsx Import React from 'REACT' Import {useDispatch} from 'REACT-Redux' import {reactionAdded} from './postsSlice' const reactionEmoji = { thumbsup : ' ' , Hooray : ' ' , Heart : ' ' , Rocket : ' ' , Eyes : ' ' } export const ReactionButtons = ( {post} ) => { const dispatch = useDispatch() const reactionButtons = Object .entries(reactionEmoji).map( ( [name, emoji] ) => { return ( < button key = {name} type = "button" className = "muted-button reaction-button" onClick = {( ) => dispatch(reactionAdded({ postId: post.id, reaction: name })) } > {emoji} {post.reactions[name]} </button > ) }) return < div > {reactionButtons} </div > } Copy code

Now, every time we click an evaluation button, the counter will increase. If we browse different parts of the application, then even if we are

<PostsList>
Click the review button in the
<SinglePostPage>
You can also see the updated data.