Understanding Redux and Redux Toolkit
Is this the right state management solution for you?
Redux toolkit is a Javascript library which builds on top of Redux to provide a state management solution for applications. It is often used in conjunction with react and react-native, but it can be used with a wide of frameworks or libraries.
What is Redux?
As applications become larger and data needs to be shared across screens and components a shared global state is required. This is where redux comes in, it stores the application state in a single immutable object called the store.
At its core, redux can be broken down into three main sections; Views, Actions, and State.
๐ฅ Views
Views are the UI elements that exist in an application like a button, list or slider. When a user interacts with these views, i.e presses a button, an action is dispatched to the store.
๐ฌ Actions
Actions are pure objects that describe the change that should be made to the store. They always contain a type
field (a string value which is a descriptive name for the action) and often a payload
which contains additional parameters.
Actions are typically dispatched by a react component but can also be dispatched by other actions in more complex cases. An example of an action to add a number to a counter can be seen below.
const addToCountAction = {
type: 'addToCount',
payload: {
numberToAdd: 100,
}
}
Action creators are functions that build and return an action. In our case, the number we want to add to the count could be dynamic so to achieve this we could have an action
creator as seen below.
const addToCountAction = (numberToAdd) => {
type: 'addToCount',
payload: {
numberToAdd
}
}
๐ State
The store
is an immutable object tree, meaning it can not be modified directly. State updates are done through reducers. A reducer
acts as an event listener to a particular action. They are pure functions that take in the current state
of the store
, an action
, and return a new state
.
To preserve the immutability principle of redux the reducer
must not modify the original state
but make immutable updates, by copying the existing state
and making changes to the copied values
const initialState = { count: 0 }
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'addToCount': {
const { numberToAdd } = action.payload;
return {
...state,
count: state.value + numberToAdd
};
}
default: {
return state;
}
}
};
Screens, buttons and other UI components can subscribe to the state and update the UI to reflect changes to the state. These three elements can be seen working together in effect in the gif below from the official redux documentation.
Advantages of Redux
๐ Centralised
Redux makes it easy to share data between different parts of the application. This is because all the state
is stored in a single place, and any part of the application can access the state
by connecting to the store
. This means we can use state from any component or screen we want.
๐ Predictable and Easy to Debug
The state
is stored in a single place and all the changes to the state are made only through actions
and reducers
. Since the state
itself is immutable and if the same state
and action
are passed to a reducer
, the same result is always produced.
This means we can easily debug the application and time travel through a log of actions
and state
updates. There are many developer-friendly tools that provide this functionality including flipper and react-devtools.
โก๏ธ Scaleable, Performant and Flexible
There is no limit to the number of actions and reducers you can have and state updates are generally fast. Additionally, components in react can be updated to only re-render when there is a visual change based on the state rather than every state update optimizing performance further.
Redux also offers the ability to add middlewares and enhancers in the configuration. Commonly used middlewares include redux-thunk which allows actions to be functions rather than just pure objects.
Where does Redux Toolkit fit in?
While redux is powerful and there are a plethora of advantages associated with it, there are a few disadvantages with it that Redux-Toolkit aims to solve. Redux-Toolkit is an optional library that provides functionality to address the most common complaints and disadvantages of redux.
๐ Verboseness
One of the main disadvantages of basic redux is its boilerplate nature. To have a state
for a value you need to have an action creator to create actions, dispatch the actions, reducers to perform state updates, and bind the state to the components.
This has to happen for each additional section of the state and this can become cumbersome and verbose. Redux toolkit aims to solve this by provides out of the box functions such as createAction()
, createReducer()
, and createSlice()
which uses both createActions()
and createReducer()
underneath and automatically generates action creators and action types.
The example below shows how the createSlice()
function can be used with the counter example below.
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
counter: 0
},
reducers: {
addToCount: (state, action) => {
state.count += action.payload.numberToAdd
}
}
})
export const { addToCount } = counterSlice.actions
export default counterSlice.reducer
๐ Prevents Common Errors
Basic redux is susceptible to a number of different issues that redux toolkit aims to solve by including commonly used libraries under the hood. This includes the following issues:
Resolving Accidental Mutations
In reducers
, it can be easy to assign a value to state
without performing a copy, especially if it is a nested value.
Redux toolkit solves this by using the Immer library in the createSlice()
and createReducer()
functions which means that direct โmutationsโ to state
are safe. Note this can be seen in the example in the addToCount
reducer above.
Redux toolkit also has an immutability check middleware in the default middleware as part of the configuration. This middleware throws an error if a mutation to the state is detected.
Checks State is Serializable
It is possible to store non-plain-JS-data values such as functions, promises and symbols in the state. This should not happen, and Redux toolkit provides an additional serializability check middleware in the default middleware that logs an error when this occurs.
๐ Simplifies Store Configuration
Store configuration for redux can be complex and verbose, especially when a lot of different middleware, enhancers and reducers are included. Redux toolkit provides a function to configure the store called configureStore()
. An example of this can be found below.
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
While this may look simple, multiple improvements are made over the default createStore()
function which it wraps. This includes automatically combining reducers (instead of having to use the combineReducers()
in base redux), enabling react-devtools by default and including default middleware such as redux-thunk (a middleware for side-effects)
A Final Word
Selecting a state management solution can be a difficult proposition as it has a direct effect on the rest of the application and can be quite difficult to change once ingrained.
Redux is an excellent choice for state management for react and react-native applications. It has an easy-to-use pattern and provides a high-performant and scalable solution, no matter the size of your application.
With Redux toolkit it becomes easy to configure and many of the issues and complaints about Redux become moot. If youโre looking for a state management solution for your application, look no further than Redux with Redux toolkit.