introduce

In Part 1: Redux Overview and Concepts, we looked at why Redux is useful, the terms and concepts used to describe different parts of Redux code, and how data flows through Redux applications.

Now, let’s look at a real-world example of how these pieces fit together.

Create a Redux Store

Open app/store.js and the code inside should look like this:

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '.. /features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})
Copy the code

The Redux store is created using the configureStore function in the Redux Toolkit. The configureStore requires that we pass in a Reducer parameter.

Our App may consist of many different capabilities, and each of these capabilities may have its own Reducer capabilities. When we call configureStore, we can pass all the different Reducer in one object. The key name in the object defines the key in the final state value.

We have a group called the features/counter/counterSlice js file, the file for export counter logic a reducer function. We can import the counterReducer function here and include it when creating a Store.

When we pass {counter: This means we want the Redux store object to have a state.counter, and we want every time an action is dispatched, The counterReducer function is responsible for determining whether and how to update the state.counter section.

Redux allows you to customize Store Settings using different types of plug-ins (” middleware “and” enhancers “). ConfigureStore by default automatically adds some middleware to the Store Settings to provide a good developer experience, and the Store is also set up so that the Redux DevTools Extension can examine its contents.

Redux Slices

Slice is a collection of Redux Reducer logic and actions to a single function in the App, usually defined together in a single file. The name comes from splitting the root Redux Store object into multiple state “slices.”

For example, in a blog application, our store setup might look like this:

import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '.. /features/users/usersSlice'
import postsReducer from '.. /features/posts/postsSlice'
import commentsReducer from '.. /features/comments/commentsSlice'

export default configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    comments: commentsReducer
  }
})
Copy the code

In this example, state.users, state.posts, and state.comments are separate “slices” of Redux state, respectively. Since the usersReducer is responsible for updating the slice state. Users, we call this the “Slice Reducer” function.

Create slice Reducers and Actions

Because we know counterReducer function from the features/counter/counterSlice js, therefore, let us piecewise view the contents of the file.

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter'.initialState: {
    value: 0
  },
  reducers: {
    increment: state= > {
      // Redux Toolkit allows us to write "mutating" logic in reducers.
      // It doesn't actually change state because it uses the IMmer library,
      // The library detects changes to "draft State" and generates new immutable states based on those changes
      state.value += 1
    },
    decrement: state= > {
      state.value -= 1
    },
    incrementByAmount: (state, action) = > {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer
Copy the code

Earlier, we saw that clicking different buttons in the UI dispatches three different types of Redux action:

  • {type: "counter/increment"}
  • {type: "counter/decrement"}
  • {type: "counter/incrementByAmount"}

We know that an action is a normal object with a Type field, which is always a string, and we usually have an “Action Creator” function that creates and returns an Action object. So where are these Action objects, type strings, and Action Creator defined?

We can write these things by hand every time. But that would be tedious. Furthermore, what really matters in Redux is the Reducer functions, and the logic they use to calculate the new state.

The Redux Toolkit has a function called createSlice that is responsible for generating the Action Type string, the Action Creator function, and the Action object. All you need to do is define a name for this slice, write an object that contains some reducer functions, and it will automatically generate the appropriate action code. The string in the name option is used as the first part of each action type, and the key name of each Reducer function is used as the second part. Therefore, the “counter” name + “increment” reduce function generates the action type {type: “counter/increment”}. (After all, if computers can help us, why write this by hand!)

In addition to the Name field, createSlice also requires us to pass the initial state value from the Reducer so that there is a state on the first call. In this case, we provide the object with a value field starting at 0.

Here we see three Reducer functions that correspond to three different action types dispatched by clicking different buttons.

CreateSlice automatically generates an Action Creator with the same name as the Reducer function we wrote. We can check this by calling one of them and looking at the return value:

console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
Copy the code

It also generates the slice Reducer function, which knows how to respond to all of these action types:

const newState = counterSlice.reducer(
  { value: 10 },
  counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}
Copy the code

The rules of the Reducer

As we said earlier, reducer must always follow some special rules:

  • They should only be based onstateandactionParameter to calculate the new state value
  • They are not allowed to modify existing onesstate. Instead, they must do so by copying what existsstateAnd make changes to the copied values for immutable updates.
  • They must not perform any asynchronous logic or other “side effects”

But why are these rules important? There are several different reasons:

  • One of the goals of Redux is to make your code predictable. It’s easier to understand how the code works and test it when you evaluate the output of a function from just the input parameters.
  • On the other hand, if a function depends on variables outside of itself or is run randomly, you never know what will happen if you run the function.
  • If a function modifies other values, including its parameters, it may change how the exception works in your application. This can be a common source of errors, such as “I updated state, but now my UI is not updating at the appropriate time!”
  • Some of the capabilities of Redux DevTools depend on the reducer following these rules correctly

The “constant update” rule is particularly important and deserves further discussion.

Reducer and constant update

Previously, we discussed “mutation” (modifying existing object/array values) and “immutability” (treating a value as one that cannot be changed).

In Redux, our Reducer is never allowed to change the original/current state value!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123
Copy the code

There are several reasons why you can’t change state in Redux:

  • It can cause errors, such as the UI not updating properly to display the latest values
  • This makes it difficult to understand why and how to update state
  • This makes writing tests more difficult
  • It breaks the ability to use “time travel debugging” properly
  • It violates the spirit of Redux’s expectations and usage patterns

So, if we can’t change the original state, how do we return the updated state?

The reducer is prompted to copy only the original values before making changes to the copy.

// ✅ This is safe because we made a copy
return {
  ...state,
  value: 123
}
Copy the code

We’ve seen that you can manually write immutable updates using JavaScript’s array/object destruct operators and other functions that return a copy of the original value. But if you think “writing immutable updates manually this way looks hard to remember and perform correctly” — yes, you’re right! 🙂

Writing immutable update logic by hand is difficult, and accidentally changing state on the Reducer is the most common mistake Redux users make.

This is why the createSlice function of the Redux Toolkit makes it easier to write immutable new ones!

CreateSlice internally uses a library called Immer. Immer wraps the data you provide with a special JS tool called Proxy, and allows you to write code that makes “changes” to the wrapped data. However, Immer will keep track of all the changes you try to make, and then use that list of changes to return safely immutable update values, as if you had written all the immutable update logic by hand.

Therefore, you want to replace the following code:

function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {... state.first.second, [action.someId]: { ... state.first.second[action.someId],fourth: action.someValue
        }
      }
    }
  }
}
Copy the code

You can write code like this:

function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}
Copy the code

It’s easier to read!

However, here are some very important things to keep in mind:

Warning you can only write “change” logic in createSlice and createReducer of the Redux Toolkit because they use Immer internally! If you write the change logic in the Reducer without Immer, it will change state and cause an error!

With this in mind, let’s go back to the actual reducer in the counter slice.

export const counterSlice = createSlice({
  name: 'counter'.initialState: {
    value: 0
  },
  reducers: {
    increment: state= > {
      // Redux Toolkit allows us to write "mutating" logic in reducers.
      // It doesn't actually change state because it uses the IMmer library,
      // The library detects changes to "draft State" and generates new immutable states based on those changes
      state.value += 1
    },
    decrement: state= > {
      state.value -= 1
    },
    incrementByAmount: (state, action) = > {
      state.value += action.payload
    }
  }
})
Copy the code

We can see that the reducer will always add 1 to state.value. Because Immer knows that we have made changes to the Draft State object, we don’t actually have to return anything here. In the same way, decrement reducer minus 1.

In both reducers, we don’t actually need to have our code view action objects. It will be passed in any way, but since we don’t need it, we can skip the parameters that declare the action as reducer.

On the other hand, the incrementByAmount Reducer does need to know how much to add to the counter value. Therefore, we make the Reducer declaration with both state and action parameters. In this case, we know that the amount we typed in the text box is already in the action.payload field, so we can add it to state.value.

Write asynchronous logic with Thunk

So far, all the logic in our application has been synchronized. Dispatch an action, the store runs the reducer and calculates the new state, and the dispatch function completes. However, the JavaScript language has many ways to write asynchronous code, and our applications often have asynchronous logic to handle things like extracting data from apis. We need a place to put asynchronous logic in the Redux App.

Thunk is a special type of Redux function that can contain asynchronous logic. Thunk is written using two functions:

  • Internal thunk function, which getsdispatchandgetStateAs a parameter
  • The external Creator function, which creates and returns the thunk function

The next function exported from counterSlice is an example of Thunk Action Creator:

// The following function, called thunk, enables us to perform asynchronous logic.
// It can be scheduled like a regular action: 'dispatch(incrementAsync(10))'.
// This calls thunk with the 'dispatch' function as the first argument. Asynchronous code can then be executed and other actions can be scheduled
export const incrementAsync = amount= > dispatch= > {
  setTimeout(() = > {
    dispatch(incrementByAmount(amount))
  }, 1000)}Copy the code

We can use them like a typical Redux Action Creator:

store.dispatch(incrementAsync(5))
Copy the code

However, using Thunk requires that the Redux-Thunk middleware (a plug-in for Redux) be added to the Redux Store when it is created. Fortunately, the configureStore function of the Redux Toolkit has been set up automatically for us, so we can continue to use Thunk here.

When you need to make an AJAX call to get data from the server, you can put that call into Thunk. This is a longer example, so you can see how it is defined:

// the outside "thunk creator" function
const fetchUserById = userId= > {
  // the inside "thunk function"
  return async (dispatch, getState) => {
    try {
      // make an async call in the thunk
      const user = await userAPI.fetchById(userId)
      // dispatch an action when we get the response back
      dispatch(userLoaded(user))
    } catch (err) {
      // If something went wrong, handle it here}}}Copy the code

We’ll see the use of Thunk in Part 5: Asynchronous logic and data retrieval.

– Thunk and asynchronous logic

We know that we do not allow any type of asynchronous logic to be placed in the Reducer. But that logic has to exist somewhere.

If we had access to the Redux store, we could write some asynchronous code and call store.dispatch() when we’re done:

const store = configureStore({ reducer: counterReducer })

setTimeout(() = > {
  store.dispatch(increment())
}, 250)
Copy the code

However, in the real Redux App, we don’t allow store to be imported into other files, especially in our React component, because it makes the code harder to test and reuse.

In addition, we often need to write asynchronous logic that will eventually be used with a store, but we don’t know which store.

The Redux Store can be extended with “middleware,” which is an add-on or plug-in that adds additional functionality. The most common reason to use middleware is to let you write code that can have asynchronous logic but still talk to store at the same time. They can also modify the store so that we can call Dispatch () and pass in non-simple action object values, such as functions or Promises.

The Redux Thunk middleware has modified the Store so that you can pass functions to Dispatch. In fact, it’s short enough that we can paste it here:

const thunkMiddleware = ({ dispatch, getState }) = > next= > action= > {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument)
  }

  return next(action)
}
Copy the code

It appears to determine whether the “action” passed to Dispatch is a function, rather than a normal Action object. If it is actually a function, it calls the function and returns the result. Otherwise, since it must be an Action object, it forwards the action to the Store.

This gives us a way to write the synchronous or asynchronous code we need, while still having access to Dispatch and getState.

There is one more feature in this file that we will discuss later when we look at the

UI component.

React counter component

Earlier, we saw what a standalone React

component looks like. Our React + Redux application has a similar

component, but differs in some ways.

We’ll start by looking at the counter.js component file:

import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount
} from './counterSlice'
import styles from './Counter.module.css'

export function Counter() {
  const count = useSelector(selectCount)
  const dispatch = useDispatch()
  const [incrementAmount, setIncrementAmount] = useState('2')

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={()= > dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={()= > dispatch(decrement())}
        >
          -
        </button>
      </div>
      {/* omit additional rendering output here */}
    </div>)}Copy the code

As with the simple React example, we have a function component called Counter that stores some data in the useState hook.

However, in our component, it looks as if we are not storing the actual current counter value as state. There is a variable called count, but it is not from useState hook.

Although React includes several built-in hooks, such as useState and useEffect, other libraries can create their own custom hooks that use React’s hooks to build custom logic.

The React-Redux library has a set of custom hooks that allow your React component to interact with the Redux store.

useuseSelectorRead the data

First, the useSelector hook enables our component to extract whatever data it needs from the Redux Store State.

Earlier, we saw that you could write a selector function that takes state as an argument and returns some part of the state value.

Our Counterslice.js has this selector function at the bottom:

// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state= > state.counter.value
Copy the code

If we had access to the Redux Store, we could retrieve the current counter value as:

const count = selectCount(store.getState())
console.log(count)
/ / 0
Copy the code

Our component cannot talk directly to the Redux Store because we are not allowed to import it into the component file. However, useSelector will talk to the Redux Store behind the scenes for us. If the selector function is passed in, it will call someSelector(store.getState()) for us and return the result.

Therefore, we can get the current store counter value by doing the following:

const count = useSelector(selectCount)
Copy the code

And we don’t have to just use the selector that’s already exported. For example, we could write the selector function as an inline argument to useSelector:

const countPlusTwo = useSelector(state= > state.counter.value + 2)
Copy the code

UseSelector rerun the selector function every time the action is dispatched and the Redux store is updated. If the selector returns a different value than last time, useSelector will ensure that our component is rerendered with the new value.

useuseDispatch dispatch action

Also, we know that if you have access to the Redux Store, you can dispatch an action using Action Creator (such as Store.dispatch (increment())). Since we don’t have access to the Store itself, we need some way to access only the Dispatch method.

UseDispatch Hook does this for us and gives us the actual dispatch method in the Redux Store:

const dispatch = useDispatch()
Copy the code

From there, we can dispatch actions when the user performs an action such as clicking a button:

<button
  className={styles.button}
  aria-label="Increment value"
  onClick={() = > dispatch(increment())}
>
  +
</button>
Copy the code

Component state and form

Now, you might be thinking, “Do I always need to put the state of all my apps into the Redux Store?”

The answer is no. The global state required by the application should be in the Redux Store. State that is needed in only one place should remain in the component.

In this example, we have an input text box where the user can enter the next number to add to the counter:

const [incrementAmount, setIncrementAmount] = useState('2')

// later
return (
  <div className={styles.row}>
    <input
      className={styles.textbox}
      aria-label="Set increment amount"
      value={incrementAmount}
      onChange={e= > setIncrementAmount(e.target.value)}
    />
    <button
      className={styles.button}
      onClick={()= > dispatch(incrementByAmount(Number(incrementAmount) || 0))}
    >
      Add Amount
    </button>
    <button
      className={styles.asyncButton}
      onClick={()= > dispatch(incrementAsync(Number(incrementAmount) || 0))}
    >
      Add Async
    </button>
  </div>
)
Copy the code

We can keep the current number string in the Redux store by dispatching an action in the input onChange handler and keeping it in our Reducer. But it didn’t do us any good. The only place to use a text string here is the

component. (Of course, there is only one other component in this example:

. However, even though we have a large application with many components, only

cares about this input value.)


Therefore, it is best to keep the value in the useState hook in the

component.

Similarly, if we have a Boolean flag called isDropdownOpen that no other component in the application cares about — then it should actually stay in that component.

In React + Redux applications, your global state should be in the Redux Store, while your local state should remain in the React component.

If you’re not sure where to put something, use the following rule of thumb to determine what kind of data to put into Redux:

  • Does the rest of the application care about this data?
  • Do you need to be able to create other derived data based on this raw data?
  • Is the same data used to drive multiple components?
  • Is it valuable to you to be able to restore this state to a given point in time (for example, time travel debugging)?
  • Do you want to cache the data (i.e., use existing state instead of rerequesting it)?
  • Do you want to keep this data consistent when hot-reloading UI components (with the possibility of losing their internal state when swapping)?

This is also an example of thinking about whether to put forms in Redux in general. Most form state probably shouldn’t remain in Redux. Instead, keep the data in the form component while you edit it, and then dispatch the Redux action to update the Store after the user is done.

One other thing to note before continuing: remember crementAsync Thunk from Counterslice.js? We use it in this component. Note that we use it the same way we use Dispatch’s other regular Action Creator. This component doesn’t care if we dispatch a normal action or start some asynchronous logic. It just knows that when you click that button, it dispatches something.

Provide the store

We have seen that our component can communicate with the Redux Store using useSelector and useDispatch hooks. But since we didn’t import the store, how do these hooks know to talk to the Redux store?

Now that we’ve seen all the different pieces of the application, it’s time to go back to the beginning of the application and see how the final pieces of the puzzle fit together.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>.document.getElementById('root'))Copy the code

We always have to call reactdom.render (
) to tell React to start rendering our root

component. To make a hook like useSelector work, we need to use a component called to pass the Redux store in the background so they can access it.

We’ve already created the Store in app/store.js, so we can import it here. Then, place the component around the entire

and pass store: .

Now, any React component that calls useSelector or useDispatch will talk to the Redux store we provide to .