Redux with Redux Toolkit

Redux with Redux Toolkit

Redux Architecture

npm Dependencies

1. @reduxjs/toolkit

  • includes redux-thunk middleware for action creators that return a function
  • includes immer library for writing immutable updates with normal mutative code, like state.todos[3].completed = true inside a reducer
  • includes redux core

2. react-redux Includes React bindings sucn as <Provider> component and useSelector hook that allow your components to access data from the redux store

Redux Toolkit (RTK)

Redux Toolkit is the recommended way of writing Redux logic. Though opinionated, it

  • Has API for easily configuring and accessing the store
  • Reduces boilerplate code connecting components
  • Has built-in capabilities for handling async events (thunks)

Provider component from RTK

  • Wrapper component for passing the redux store to your React components
  • Accepts a store property with the value created from Redux Toolkit's configureStore API
  • When you pass in an object like {counter: counterReducer} within configureStore, it will create a state.counter section in your Redux state object

createSlice() method from RTK

  • A "slice" is a collection of Redux reducer logic and actions for a single feature in your app. It represents the logical grouping of your application state, e.g. each key is a slice if your state object is state = { user, tasks, orders }.
  • Each slice is defined in a single file using the createSlice method from RTK which will create your actions and reducers
  • The convention for naming reducers is to use past tense to describe something that has happened, e.g. createdItem, editedPost, etc...
  • createSlice and createReducer uses the immer library internally to allow you to write simple mutative code
  • extraReducers parameter allows you to define custom action types that are not normally generated by createSlice. It is specially useful for Promise values that require state changes before, after or when an error is thrown on a particular request.
  • The string from 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. For example, the "counter" name + the "incremented" reducer function generated an action type of {type: "counter/incremented"}.
  • If you need extra data manipulation within a reducer, you can use the prepare option for additional set up

useSelector() from React Redux

  • hook for accessing a slice from the Redux state

  • it accepts a "selector" function as its argument, e.g. useSelector(state => state.key.value)

  • it's the replacement for each individual state key in mapStateToProps

    // FROM
    const mapStateToProps = (state, ownProps) => {
      return {
        todo: selectTodoById(state, ownProps.todoId),
        activeTodoId: selectActiveTodoId(state)
      }
    }
    
    // TO
    const todo = useSelector(state => state.todo)
    const activeTodoId = useSelector(state => state.activeTodoId)

useDispatch() from React Redux

  • hook that returns a reference to dispatch from the redux store for "dispatching" actions

  • React hook lint rules is unaware the return value of useDispatch() is stable. Pass it as a dependency of useEffect and useCallback to get around it

    // https://react-redux.js.org/api/hooks#usedispatch
    import { useDispatch } from 'react-redux'
    export const Todos = () => {
      const dispatch = useDispatch()
      useEffect(() => {
        dispatch(fetchTodos())
        // Safe to add dispatch to the dependencies array
      }, [dispatch])
    }

createasyncthunk()

createAsyncThunk allows you to handle Promise-based actions. By using it, you automatically generate action creators for "pending" and "fulfilled" events. Though you don't have to handle the change in state, it is generally a good practice to do so with the use of extraReducers. Note that if you choose not to handle state change, at least provide an initial check if the data has been loaded else you'll end up repeating the request every render.

createAsyncThunk accepts two arguments:

  1. A string that will be used as the prefix for the generated action types
  2. A "payload creator" callback function that should return a Promise containing some data, or a rejected Promise with an error
export const fetchTasks = createAsyncThunk('task/fetchedTasks', async()=> {
  const response = await fetch('/tasks.json');
  return response.json();
});

React Router and Redux

The rule in a React + Redux app is your global state should go in the Redux store, and your local state should stay in React components. With that in mind, React Router top-level components access to the store should be determined on a per-need basis. If only 1 route relies on a specific data, then there's no need to put that data in your global state. You can just fetch it at the time the route component loads. There are several optimization techniques you can apply to improve the performance of your page.

Complete Code Example