Editor’s note: This article was updated on 22 April 2022 to include up-to-date information about Redux-Saga and Redux Toolkit.
When Redux first appeared on the frontend horizon, many expected it to be a solution to all our state management problems. Its association with React was so deep that people began to believe that React was somehow incomplete without Redux, though this idea was categorically rejected by Redux creator Dan Abramov.
Gradually, developers began to realize Redux’s limitations, which led to a chain of debate around whether there are better ways to manage global state — and, indeed, whether Redux is a viable solution to begin with.
createSlice
Many of the arguments against Redux were born of a number of opinions and “best practices” that later came to be viewed as requirements. (In actuality, Redux in its bare form is quite simple and easy to understand.) But one of the most popular and enduring critiques is the amount of code Redux adds to the app, which many feel is unnecessary.
These debates led to the development of Redux Toolkit (RTK), “the official, opinionated, batteries-included toolset for efficient Redux development.” The Redux team has put a huge amount of effort into this and, without a doubt, has produced a remarkable result.
RTK resolves many of the arguments related to boilerplate and unnecessary code. As mentioned in its official docs, it helps to solve three major problems people had with Redux:
Here we will see how we can leverage RTK’s API to make our Redux applications smaller yet still powerful. We will use a React and an RTK template to bootstrap our application.
Our app will have the following architecture for containers:
Later on, we will install Redux-Saga and see how it can be used for async tasks.
One important note: Redux Thunk is available with RTK as a default option for async tasks and is highly recommended for simple data fetching tasks. You can find its guide here. In this post, however, we will use Redux-Saga for better understanding of middleware integration with RTK. It would be well worth reading through this post if your unfamiliar with Redux-Saga.
You can find the source code for the application here.
One of Redux’s most discussed flaws is the effort required to integrate it into an existing application. With Redux Toolkit comes the option to bootstrap a React app with Redux Toolkit included.
To do so, run the following command in your terminal:
npx create-react-app my-redux-app --template redux
When the download is complete, open up the application in your text editor, and we will examine the key file that makes the Redux store
function:
// store.js import { configureStore } from '@reduxjs/toolkit' import counterReducer from '../features/counter/counterSlice' export const store = configureStore({ reducer: { counter: counterReducer, }, // other options e.g middleware, go here })
The Redux store — located in app/store.js
— is the central station of our Redux Toolkit application. This file makes use of RTK’s configureStore
API, which, according to the RTK docs, is a friendly abstraction over the standard Redux createStore
function. It adds good defaults to the store setup for a better development experience.
configureStore
accepts a single configurations object with multiple parameters, the most important being reducer
.
reducer
is an object that stores different slices
in our application and reflects it in the Redux store, as seen through the Redux DevTools extension.
In our template, a counterSlice
is added to reducer
object — more explanation on what slices are soon.
To make data from our store accessible to all components in our application, the Provider
component from react-redux
is used to wrap the entire application, taking in the Redux store as the value for the store
prop:
// index.js 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'; ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );
Though Redux is not dependent on how your files are structured, it plays an important role in the maintenance of large-scale applications. It’s better to group files based on features rather than file types.
The widely adopted ducks pattern suggests keeping all Redux functionality in a single file. This file will export a reducer function by default along with all actions, creators, and constants, if required. It also suggests the pattern for action types.
The name comes from the last part of Redux — dux — and has become a highly recommended pattern for Redux applications.
RTK follows the ducks pattern and combines reducers, actions, and constants in one file called a slice. Each slice will provide an initial state and a reducer function for an object in store:
// counterSlice.js import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { fetchCount } from './counterAPI'; const initialState = { value: 0, status: 'idle', }; // asynchronous function with createAsyncThunk export const incrementAsync = createAsyncThunk( 'counter/fetchCount', async (amount) => { const response = await fetchCount(amount); return response.data; } ); // Redux Toolkit slice export const counterSlice = createSlice({ name: 'counter', initialState, // The `reducers` field lets us define reducers and generate associated actions 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; }, }, // The `extraReducers` field lets the slice handle actions defined elsewhere, // including actions generated by createAsyncThunk or in other slices. extraReducers: (builder) => { builder .addCase(incrementAsync.pending, (state) => { state.status = 'loading'; }) .addCase(incrementAsync.fulfilled, (state, action) => { state.status = 'idle'; state.value += action.payload; }); }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; // more code... export default counterSlice.reducer;
The counterSlice
file uses the createSlice
method from RTK. This method returns an object with reducers and actions that can be used for injection with other middleware.
createSlice
Redux Toolkit can replace a series of actions/reducers/constants using its createSlice
API. It is a helper method to generate a store slice. It takes the name of the slice, initial state, and a reducer function to return a reducer, action types, and action creators.
In return, it gives us the name of the slice, action creators, and reducer functions. All of these can be used for injection of the slice, dispatching actions, and other cases, depending upon implementation.
Looking at counterSlice.js
, we can see that createSlice
returns an object with a name and reducers that can be dispatched from anywhere in our application like so:
// Counter.js import React, { useState } from 'react'; import { useDispatch } from 'react-redux'; import { increment, } from './counterSlice'; import styles from './Counter.module.css'; export function Counter() { const dispatch = useDispatch(); return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())} > + </button> </div> </div> ); }
Here are a few details on the parameters of the createSlice
method:
name: string
A name, used as the ID of the slice in store and also as a prefix for action types of this reducer. This uniquely identifies slices in store.
initialState: any
The initial state for the reducer.
reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
Reducers in RTK are objects, where keys are used as action types and functions are reducers associated with these types. The key is also used as a suffix for action types, so the final action type becomes ${name}/${actionKey}
.
Under the hood, this object is passed to [createReducer](https://redux-toolkit.js.org/api/createReducer)
, an RTK utility to simplify the definition of reducers. It allows us to define reducers as a lookup table of functions to handle each action type. This helps to avoid the boilerplate code of action creator functions.
It is recommended to use immutable state management in reducers, and Immer is one of the most popular libraries to do so. RTK allows you to mutate state using dot notation and uses Immer under the hood. More precisely, createSlice
and createReducer
wrap your reducer functions with produce
from Immer.
extraReducers
This is also a case reducer function, but for actions other than this slice. Each slice reducer owns its slice in the global store, but it can respond to any action type, including those generated by another slice.
This API will allow us to mutate the state of the current slice on dispatch of actions generated by another slice. The reducer will pass through the same createReducer
API, allowing safe mutation.
With React-Redux (already included in the template) we gain access to two important Hooks: useSelector
and useDispatch
. These Hooks let us read data from the Redux store and dispatch actions from any slice into our components:
// Counter.js import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { decrement, increment, incrementByAmount, incrementAsync, incrementIfOdd, selectCount, } from './counterSlice'; import styles from './Counter.module.css'; export function Counter() { const count = useSelector(selectCount); const dispatch = useDispatch(); const [incrementAmount, setIncrementAmount] = useState('2'); const incrementValue = Number(incrementAmount) || 0; return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Decrement value" onClick={() => dispatch(decrement())} > - </button> <span className={styles.value}>{count}</span> <button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())} > + </button> </div> <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(incrementValue))} > Add Amount </button> <button className={styles.asyncButton} onClick={() => dispatch(incrementAsync(incrementValue))} > Add Async </button> <button className={styles.button} onClick={() => dispatch(incrementIfOdd(incrementValue))} > Add If Odd </button> </div> </div> ); }
We have discussed two of the major APIs in RTK that are, IMHO, useful for most cases. Under the hood, these APIs make use of other utilities, which are also available individually:
createAction
With Redux, we need to define a constant that represents a type of action, and then a function to create an action of that type. Though this isn’t required by Redux, it helps us keep different store files in sync with each other.
With this API, the hassle of multiple declarations is gone. It takes the action type and returns the action creator for that type. The returned action creator is invoked with an argument, which will be placed as a payload.
Here is a simple example:
const increment = createAction('counter/INCREMENT'); // increment() -> { type: 'counter/INCREMENT' } // increment(5) -> { type: 'counter/increment', payload: 3 } // increment.toString() -> 'counter/INCREMENT' // console.log(increment) -> counter/INCREMENT
For more complex cases, it also accepts the other argument, a function for custom action creation logic. This example makes it easy to grasp:
const addTodo = createAction('todos/ADD, function prepare(text) { return { payload: { text, createdAt: new Date().toISOString() } } }) console.log(addTodo('Some text')) /** * { * type: 'todos/ADD', * payload: { * text: 'Some text', * createdAt: '2019-10-03T07:53:36.581Z' * } * } **/
createReducer
As mentioned above, this is one of the special sauces in the createSlice
method. It helps us to write a simpler reducer. This removes boilerplate code associated with case reducers and allows us to write reducers as a function lookup table to handle each action type. Using the power of Immer, it makes mutating state more intuitive.
It takes two arguments: first is the initial state, the other is the object mapping from action types to reducers.
A simple counter reducer that may have formerly looked like this:
function counterReducer(state = 0, { type, payload }) { switch (type) { case 'increment': return state + payload case 'decrement': return state - payload default: return state } }
Would now look like this:
const counterReducer = createReducer(0, { increment: (state, { payload }) => state + payload, decrement: (state, { payload }) => state - payload })
Actions created using the createAction
API can be used as keys.
Redux Saga is a middleware library used to allow a Redux store to interact with resources outside itself asynchronously. This includes making HTTP requests to external services, accessing browser storage, and executing I/O operations. These operations are also known as side effects.
Although asynchronous functions can be created by RTK’s createAsyncThunk
, sagas are more powerful and easier to test, but they introduce many new concepts that can be a bit overwhelming if you’re also learning other technologies. This StackOverflow post succinctly explains the difference between Redux Thunk and Redux-Saga.
Run the following command in your terminal to install Redux-Saga and Axios, which will be used for data fetching:
npm install Redux-Saga axios
Now you can use Redux-Saga in your Redux store like so:
// store.js import { configureStore } from '@reduxjs/toolkit' import createSagaMiddleware from 'Redux-Saga' import counterReducer from '../features/counter/counterSlice' import saga from '../sagas/saga' let sagaMiddleware = createSagaMiddleware() const middleware = [sagaMiddleware] export const store = configureStore({ reducer: { counter: counterReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware), }) sagaMiddleware.run(saga)
Redux-Saga provides a createSagaMiddle
function that we use to instantiate sagaMiddleware
. Because the middleware
parameter in configureStore
API is an array of the existing middleware, we can get the current middleware with the getDefaultMiddleware
callback and concatenate our new Saga middleware to the array.
Finally, we use the run
function on sagaMiddleware
to our saga, which I’ve created in the following path: sagas/saga.js
.
Redux-Saga
offers a number of effects, which are instructions given to the middleware to perform certain operations. Some of these include:
takeEvery (actionType, saga)
spawns a saga
on each action dispatched to the store that matches actionType
, e.g., FETCH_USER_DETAILS
call (fn, …args)
creates an effect description that instructs the middleware to call the function fn
with args
as arguments, if anyput: (action)
creates an effect description that instructs the middleware to schedule the dispatching of an action to the storeApplying this to our application, we can create a saga that asynchronously fetches a random number from an API and then uses the incrementByAmount
reducer from counterSlice
to increment the counter value by the value from the external API:
// saga.js import { call, takeEvery, put } from 'Redux-Saga/effects' import Axios from 'axios' import { incrementByAmount } from '../features/counter/counterSlice' import { sagaActions } from './sagaActions' // function uses axios to fetch data from our api let callAPI = async ({ url, method, data }) => { return await Axios({ url, method, data, }) } export function* fetchNumberSaga() { try { let result = yield call(() => callAPI({ url: 'http://www.randomnumberapi.com/api/v1.0/random?min=100&max=1000&count=1', }) ) yield put(incrementByAmount(result.data[0])) } catch (e) { yield put({ type: 'NUMBER_SAGA_FAILED' }) } } export default function* rootSaga() { yield takeEvery(sagaActions.FETCH_NUMBER_SAGA, fetchNumberSaga) }
Here, the call
effect is used to make a request to an external API, and put
is used to update the store with a reducer function.
After the preceding asynchronous request has been made, takeEvery
creates a saga that watches for any action with type FETCH_NUMBER_SAGA
referenced as sagaActions.FETCH_NUMBER_SAGA
from sagaActions.js
:
// sagas/sagaActions.js export const sagaActions = { FETCH_NUMBER_SAGA: 'FETCH_NUMBER_SAGA', }
With our first saga now setup, we can dispatch it from the Counter
component as such:
// Counter.js import React, { useState } from 'react' import { useSelector, useDispatch } from 'react-redux' import { decrement, increment, incrementByAmount, incrementAsync, incrementIfOdd, selectCount, } from './counterSlice' import styles from './Counter.module.css' import { sagaActions } from '../../sagas/sagaActions' export function Counter() { const count = useSelector(selectCount) const dispatch = useDispatch() const [incrementAmount, setIncrementAmount] = useState('2') const incrementValue = Number(incrementAmount) || 0 return ( <div> <div className={styles.row}> <button className={styles.button} aria-label='Decrement value' onClick={() => dispatch(decrement())} > - </button> <span className={styles.value}>{count}</span> <button className={styles.button} aria-label='Increment value' onClick={() => dispatch(increment())} > + </button> </div> <div className={styles.row}> <input className={styles.textbox} aria-label='Set increment amount' value={incrementAmount} onChange={(e) => setIncrementAmount(e.target.value)} /> {/* buttons */} {/* add random number asynchronously with redux saga */} <button className={styles.button} onClick={() => dispatch({ type: sagaActions.FETCH_NUMBER_SAGA })} > Add Random number with Saga </button> </div> </div> ) }
As we have seen, Redux Toolkit has eliminated many of the arguments raised against Redux. It also helps to bridge the knowledge gaps around good practices and patterns. It can be super helpful for large-scale applications especially since it can also be used within existing Redux applications.
Whether RTK will be able to resolve all the debate around Redux and its usage is still a question, but without a doubt, it looks like a good leap forward in the right direction. Let us know what you think of it in the comments section.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
3 Replies to "Smarter Redux with Redux Toolkit"
Thanks for writing this article!
A few quick thoughts:
– I specifically [chose thunks for use in RTK instead of sagas for a variety of reasons](https://blog.isquaredsoftware.com/2020/02/blogged-answers-why-redux-toolkit-uses-thunks-for-async-logic/). In general, sagas are a great power tool, but most Redux apps don’t actually need them (and especially not just for basic data fetching).
– Similarly, I’ve never been particularly fond of the whole “container/presentational” concept. It’s not _bad_, but the community has way-over-obsessed on it. That’s why Dan Abramov has said [he no longer supports that idea](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0), and when I rewrite the Redux core tutorial, I’ll be removing that phrasing entirely.
– “Ducks” are not a competitor to “containers”, because “containers” were always about component structure. Ducks are an alternative to having separate folders for actions/reducers/constants, or even having separate files for those in a single feature folder.
– I’d suggest expanding the post to show some examples of `createSlice` in action, and especially how Immer makes immutable updates of objects a lot simpler.
Hi Mark, Thanks a lot for taking out time and giving such valuable feedback.
1) Redux Thunk: I have already mentioned that Thunk is default middleware for async tasks. I specifically mentioned saga since thunk has been discussed in docs and many other tutorials. Also by this I was able to explain option to add middleware.
Remedy: I am making it more prominent that why I opted for Saga & Thunk is the recommended approach.
2/3) I mentioned this update on Dan’s article in my previous posts, but as a concept (Segregating store and presentational layer of app), I feel this is still very helpful in many large scale apps. Since I considered example of react-boilerplate I went with CP pattern. CP already had concept of grouping reducer/action/constants based on feature, I feel ducks took that idea a step ahead. I am still open to updates in post if you feel this might lead to any misconceptions in community.
Remedy: I am rephrasing it upgrade section
4) Though I am not a big fan of long posts, unless they are coming from you 😉 I will add code of createSlice as a example. About Immer and immutability, post already shed some light on this adding more here will make it a little heavy IMHO.
Once again thanks for sharing these findings.
Ohh yes, compare how much simpler you can do all of the above with Hookstate: https://hookstate.js.org/ (Disclaimer: I am a maintainer). It will be fast by default without any sorts of selectors. It will be smaller size as the core package is powerful without any extra libs. But it is also extendable with plugins, and standard plugins assist with persistence, state validation, etc… What do you think?