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, likestate.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'sconfigureStore
API - When you pass in an object like
{counter: counterReducer}
withinconfigureStore
, it will create astate.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
andcreateReducer
uses theimmer
library internally to allow you to write simple mutative codeextraReducers
parameter allows you to define custom action types that are not normally generated bycreateSlice
. It is specially useful forPromise
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 ofuseEffect
anduseCallback
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:
- A string that will be used as the prefix for the generated action types
- 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.