introduce

In Part 1: Redux Overview and Concepts, we looked at how Redux helps us build maintainable applications by giving us a central place to put global application state. We also discussed the core concepts of Redux, such as the Dispatch action object, using the Reducer function that returns a new state value, and writing asynchronous logic using thunk. Part 2: In the Redux application structure, we saw how apis such as configureStore and createSlice in the Redux Toolkit and Provider and useSelector in react-Redux work together. Allows us to write Redux logic and interact with the logic in the React component.

Now that you have some idea of what those parts are, it’s time to put that knowledge into practice. We will build a small social media feed application that will contain many features that demonstrate some real-world use cases. This will help you understand how to use Redux in your own applications.

warning

The sample application is not meant to be a complete production-ready project. The goal is to help you learn about the Redux API and typical usage patterns, with some limited examples to point you in the right direction. In addition, some of the early works we built will be updated in the future to show a better way of doing things. Please read through the tutorial to understand all the concepts being used.

The main flow of posts

The main function of our social media feed application is a list of posts. We’ll add more snippets to this feature as we go along, but first, our first goal is to display only a list of post entries on the screen.

Create Posts Slice

The first step is to create a new Redux “slice” that will contain the data we published. Once we have this data stored in the Redux Store, we can create the React component to display the data on the page.

Within SRC, create a new Features folder, place a Posts folder within Features, and add a new file called postsslice.js.

We will use the createSlice function of the Redux Toolkit to make a Reducer function that knows how to process our posts data. The Reducer function needs to include some initial data so that the Redux store loads these values when the application starts.

Now, we’ll create an array with some fake POST objects inside so we can start adding UI.

We’ll import createSlice, define our initial posts array, pass it to createSlice, and export the posts Reducer function generated by createSlice for us:

import { createSlice } from '@reduxjs/toolkit'

const initialState = [
  { id: '1'.title: 'First Post! '.content: 'Hello! ' },
  { id: '2'.title: 'Second Post'.content: 'More text'}]const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {}})export default postsSlice.reducer
Copy the code

Each time a new slice is created, its Reducer function needs to be added to the Redux store. We’re already creating the Redux Store, but right now it doesn’t have any data inside it. Open app/store.js, import the postsReducer function, and update the call to configureStore so that postsReducer is passed as a Reducer field named posts:

import { configureStore } from '@reduxjs/toolkit'

import postsReducer from '.. /features/posts/postsSlice'

export default configureStore({
  reducer: {
    posts: postsReducer
  }
})
Copy the code

This tells Redux that we want our top-level state object to have a field named posts in it, and that the postReducer function updates all the data for state.posts during the Dispatch action.

We can verify that this method works by opening the Redux DevTools Extension and viewing the current state content:

Display the Posts list

Now that we have some posts data in our Store, we can create a React component that displays a list of posts. All code related to our posts stream functionality should be in the Posts folder, so go ahead and create a new file called postslist.js.

If you want to render the posts list, you need to get the data from somewhere. The React component can read data from the Redux store using the useSelector hook in the React-Redux library. The “selector function” you write will be called with the entire Redux state object as an argument and will return the specific data required by that component from the store.

Our original PostsList component would read the state.posts value from the Redux store, then loop through all posts and display them on the screen:

import React from 'react'
import { useSelector } from 'react-redux'

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>
    </article>
  ))

  return (
    <section className="posts-list">
      <h2>Posts</h2>
      {renderedPosts}
    </section>)}Copy the code

Then, we need to update the route in app.js to display the PostsList component instead of the “welcome” message. Import the PostsList component into app.js, and replace the welcome text with . We’ll also wrap it in React Fragment, because we’ll be adding something else to the home page soon:

import React from 'react'
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect
} from 'react-router-dom'

import { Navbar } from './app/Navbar'

import { PostsList } from './features/posts/PostsList'

function App() {
  return (
    <Router>
      <Navbar />
      <div className="App">
        <Switch>
          <Route
            exact
            path="/"
            render={()= > (
              <React.Fragment>
                <PostsList />
              </React.Fragment>)} / ><Redirect to="/" />
        </Switch>
      </div>
    </Router>)}export default App
Copy the code

After the addition, our app’s home page should now look like this:

One step further! We’ve added some data to the Redux Store and displayed it on the screen in the React component.

Add new Posts

It’s nice to see other people’s posts, but we want to be able to write our own. Let’s create an “Add new Post” form, let’s write the post and save it.

We’ll start by creating an empty form and adding it to the page. We then connect the form to the Redux Store so that we can add a new post when we click the Save Post button.

Add a new post form

Create addPostform.js in our Posts folder. We will add text input for the post title and text input area for the body of the post:

import React, { useState } from 'react'

export const AddPostForm = () = > {
  const [title, setTitle] = useState(' ')
  const [content, setContent] = useState(' ')

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

  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
          id="postContent"
          name="postContent"
          value={content}
          onChange={onContentChanged}
        />
        <button type="button">Save Post</button>
      </form>
    </section>)}Copy the code

Import this component into app.js and add it above the component:

<Route
  exact
  path="/"
  render={() = > (
    <React.Fragment>
      <AddPostForm />
      <PostsList />
    </React.Fragment>)} / >Copy the code

You should see the table displayed on the page below the title.

Save the input to the post

Now, let’s update the Posts Slice to add the new posts entry to the Redux store.

Our Posts Slice handles all updates to our posts data. Inside the createSlice call, there is an object called Reducers. Now, it’s empty. We need to add a reducer function to it to handle adding posts.

Inside reducers, add a function called postAdded that will take two parameters: the current state value and the action object that has been dispatched. Since Posts Slice only knows about the data it’s responsible for, the state argument will be its own array, not the entire Redux State object.

The Action object will take our new POST object as the action.payload field and put it in the state array.

When we write the postAdded Reducer function, createSlice will automatically generate an “Action Creator” function with the same name. When the user clicks Save Post, we can export the Action Creator and use it in the UI component to dispatch the action.

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

export const { postAdded } = postsSlice.actions

export default postsSlice.reducer
Copy the code

warning

Remember: The reducer function must always create new state values by copying! It is safe to call change functions like array.push () or modify object fields like state.somefield = someValue in createSlice() because it uses the Immer library to internally convert these changes into safe immutable new, But do not attempt to change any data other than createSlice!

Dispatch “Post Added” Action

Our AddPostForm has text entry and a “Save Post” button, but that button hasn’t done anything yet. We need to add a click handler that dispatches postAdded Action Creator and passes a new POST object containing user-written titles and content.

Our POST object also needs to have an ID field. Now, our initial test post uses some fake numbers as ids. We could write some code to figure out what the next incremental ID number should be, but it would be better if we generated a random unique ID. The Redux Toolkit has a Nanoid function that you can use for this.

To dispatch the action from the component, we need to access the Store’s Dispatch function. We do this by calling the useDispatch hook of React-Redux. We also need to import postAdded Action Creator into this file.

Once we have the Dispatch function in our component, we can call Dispatch (postAdded()) in the click handler. We can get the title and content values from the React component’s useState hook, generate a new ID, and then put them in a new POST object, which we pass to postAdded().

import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'

import { postAdded } from './postsSlice'

export 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>
        {/* omit form inputs */}
        <button type="button" onClick={onSavePostClicked}>
          Save Post
        </button>
      </form>
    </section>)}Copy the code

Now, try entering a title and some text, and click Save Post. You should see the new item for that post in the list of posts.

Congratulations to you! You just built your first working React + Redux application!

This shows the full Redux data flow cycle:

  • Use our list of postsuseSelectorThe initial set of posts is read from the Store and the initial UI is rendered
  • We dispatchpostAddedAction, which contains data for the new post entry
  • Posts Reducer saw itpostAddedAction and updates the Posts array with the new entry
  • The Redux Store tells the UI that some data has changed
  • The posts list reads the updated array of posts and rerenders to display the new posts

All the new functionality we’ll add will follow the same basic pattern you see here: add state slice, write reducer functions, dispatch actions, and render the UI based on the data in the Redux Store.

We can check the Redux DevTools Extension to see our Dispatch action and see how the Redux state is updated in response to that action. If you click the “Posts /postAdded” entry in the action list, the” Action “TAB should look like this:

The “Diff” tag should also show us that a new item has been added to state.posts, which is in index 2.

Note that our AddPostForm component has some React useState hooks inside to keep track of the title and content values entered by the user. Remember, the Redux Store should only contain data that is considered “global” to your application! In this case, only AddPostForm needs to know the latest value of the input field, so we want to keep that data in the React component state rather than trying to keep temporary data in the Redux Store. When the user completes the form action, we dispatch a Redux action to update the store based on the final value entered by the user.