The web content provides a comprehensive guide on integrating Redux Toolkit (RTK) into React and React Native applications, emphasizing its simplicity and benefits over traditional Redux, including handling asynchronous actions and API integration.
Abstract
The article, last updated on October 2, 2021, offers an in-depth tutorial on using Redux Toolkit (RTK) in React and React Native applications. It highlights RTK's ability to simplify Redux implementation by co-locating reducers, actions, and state in a single file, eliminating the need for action type constants, and providing out-of-the-box support for middleware like Thunk and Redux DevTools. The guide covers RTK integration steps, the concept of slices, and how to handle side-effects, including asynchronous requests and REST API interactions. It also discusses the use of createAsyncThunk for managing API call states and provides a demo with a CodeSandbox example. The author shares personal opinions on best practices, favoring traditional thunk functions over createAsyncThunk for clarity and simplicity.
Opinions
The author advocates for the use of Redux Toolkit over traditional Redux due to its simplified approach and reduced boilerplate.
Slices are praised for their cleanliness and ease of maintenance, as they encapsulate all related reducers, actions, and state.
The author suggests that local component state should be used when possible instead of Redux for simplicity.
While acknowledging RTK's built-in support for Thunk middleware, the author prefers using traditional thunk functions for side-effects and asynchronous logic.
The author finds createAsyncThunk to be complex and suggests it should only be used when status tracking for async operations is necessary, otherwise, traditional thunks are recommended.
Redux-persist is recommended as an essential tool for persisting state across sessions in React and React Native applications.
The author encourages readers to support their work by buying them a coffee, indicating a preference for direct contributions over other forms of remuneration.
Redux Toolkit RTK in react js web and react-native applications — Simplify redux with react
We will use the same to keep the understanding consistent.
What is a Slice:
A slice is created using ‘createSlice’ API from redux-toolkit.
It requires a string name to identify the slice, an initial state value, and one or more reducer functions at minimum.
Unlike traditional redux, slice reducers allow us to mutate the original state. It uses immer library internally to make this possible.
Clean: Code is so clean and you have reducers, actions, constants everything at one place to maintain.
You can create as many feature slices as required.
Make sure, create redux states wisely. Local component state is always the best choice.
EXAMPLE OF A COUNTER SLICE:
import { createSlice } from'@reduxjs/toolkit'
const initialState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
// Action creators are generated for each case reducer functionexportconst { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
How to create a new state in store?
By new state, I mean a state.counter, state.employee, state.config params that we wish to create in store as per the application requirement.
Every feature that requires data sharing or props drilling, must use redux. For simplicity, we create individual feature slices and they will be registered in our redux store.
Thus, each feature will have its own state making it easy for maintenance.
Async requests / REST APIs / side-effects in actions:
This is the most critical and interesting part.
Majority of the user applications will require API/SDK integration to fetch remote data asynchronously. (REST APIs, Firebase SDK, async processing, other side-effects)
Thunk being the traditional thunk action returning a function with dispatch param is easy to use and requires no new learning.
It can be used as is even with redux toolkit.
Thunk functions need to be at the end of Slice, as the required actions need to be exported before using them in thunk.
Example of a Thunk:
// First, define the reducer and action creators via `createSlice`
const usersSlice = createSlice({
name: 'users',
initialState: {
loading: 'idle',
users: [],
},
reducers: {
usersLoading(state, action) {
// Use a "state machine" approach for loading state instead of booleans
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
usersReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle'
state.users = action.payload
}
},
},
})
// Destructure and export the plain action creatorsexportconst { usersLoading, usersReceived } = usersSlice.actions
// Define a thunk that dispatches those action creatorsconst fetchUsers = () => async (dispatch) => {
dispatch(usersLoading())
const response = await usersAPI.fetchAll()
dispatch(usersReceived(response.data))
}
createAsyncThunk:
createAsyncThunk should be used only for getting the real time status of the async function from one of these 3 options: “pending”, “fulfilled” or “rejected”.
Only advantage of using “createAsyncThunk” over traditional Thunk is that you can skip writing 3 lines of code dispatching pending, success & error actions.
Honestly, I felt createAsyncThunk is very confusing and difficult to learn.
It adds less value and comparatively creates more complexity. (Totally a personal opinion. If the feature is made, there must be a thought process and a kind of requirement that I might not have come across.)
Thus, if you are in the same boat facing difficulty in understanding createAsyncThunk, NO WORRIES! You can simply skip and continue using traditional Thunk action functions.
Although, no harm in still understanding the working of createAsyncThunk:
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add userto the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the appdispatch(fetchUserById(123))
How it works:
First param of createAsyncThunk is “string”. It can be anything that makes it easy for you to understand.
This string is used to create loading, success & error action.types behind the scenes. Most of the time, you will never require to use them as you are not going to dispatch it manually ever. Do not waste more time understanding more about this parameter (I did).
Second param is an async function or a promise that resolves asynchronously.
extraReducers: This is a new property required under reducers to handle createAsyncThunk functions.
All the reducers (switch cases) related to pending, fulfilled & rejected statuses of the createAsyncThunk are to be defined inside extraReducers. If you skip any of the status handling (like in the example), there is nothing wrong in it. They are optional.