DEV Community

Cover image for Redux is Dead: Long Live Redux Toolkit
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on • Originally published at blog.openreplay.com

Redux is Dead: Long Live Redux Toolkit

by author Victoria Lo

In this article, let's learn about Redux Toolkit, or as the development team calls it, "the official, opinionated, batteries-included toolset for efficient Redux development".

What is Redux & How it Works

Most web developers would probably have heard of Redux, a popular state management library. It first came to the front-end world in 2015 as the revolutionary state management solution built by Dan Abramov and Andrew Clark.

In front-end frameworks like React, Angular or Vue, each component internally manages their own states. As the app gets more complex, managing states across many components becomes tedious and difficult. Redux became the solution to this issue.

Redux works by providing a centralized 'store', which holds all the states within the app. Each component in the app can access this store without having to pass props around in the component tree.

redux-store.png
Image from codecentric

The Redux Flow

The typical Redux flow is as follows:

  1. A user interacts with the View to trigger a state update
  2. When a state update is required, the View dispatches an action
  3. The reducers receive the action from the dispatch and updates the state in the Store according to what is described by the action
  4. The View is subscribed to the Store to listen for state changes. The changes are notified via the subscription methods and the View updates its UI accordingly

redux-flow.png
Image from esri

The Redux flow is made up of 3 main components: Actions, Reducers and Store. Understanding the relationship between these components is necessary to know how Redux works.

Actions are JavaScript objects with a required type property and can include custom properties where needed. They are only used to describe what happened to the state, they are not responsible for changing them. Some examples of actions:

//action to add a todo item
{ type: 'ADD_TODO', text: 'This is a new todo' } 
//action that pass a login payload
{ type: 'LOGIN', payload: { username: 'foo', password: 'bar' }} 
Enter fullscreen mode Exit fullscreen mode

The type of an action is simply a string that describes the action, and the added properties are information that are needed to update the state. An action is dispatched via the store.dispatch(action) method, and reducers handle updating the state.

Reducers are pure functions that takes in the current value of a state, performs the operations on it as instructed by the action, then outputs the new value of the state. They are the ones responsible for changing the value of the state. Here's a simple example of a reducer function:

//takes in the current state and action
//updates the value based on the action's type
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'INCREASE':
      return { value: state.value + 1 }
    case 'DECREASE':
      return { value: state.value - 1 }
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, the state will be updated in the Store. The Store is where all the states are managed. It can be created in a single line:

const store = createStore(myComponent);
Enter fullscreen mode Exit fullscreen mode

The components must be subscribed to the Store to listen for state updates to render the states correctly in the UI. The store.subscribe() method adds a change listener that will be called whenever an action is dispatched.

Why Redux Toolkit

At this point, we can see why Redux was a popular option for state management. Its pattern makes states predictable, as reducers are pure functions, which means the same state and actions passed will always result in the same output.

It is also easily maintainable and scalable due to the strict organization on how each part in the Redux flow should behave and work. Also, there are many other benefits such as efficient testing, easy debugging and better performance that Redux brings to the table.

However, this flexible and high-level state management library comes with a few challenges:

  1. Too much code to configure Store to optimized levels/best practices
  2. Too much boilerplate code makes code less clean and efficient
  3. Too many packages needed to install to build scalable apps
  4. Writing actions and reducers becomes more complex and cumbersome in huge applications

To address these challenge, the Redux team came up with Redux Toolkit, the official recommended approach for writing Redux logic. It aims to speed up Redux development by including Redux Core with the packages that they think are essential to build a Redux app. It is an opinionated derivative of Redux, with many best practice configurations for Redux beginners or developers who want simple, fast and clean Redux code.

So let's get started with Redux Toolkit and set it up with a new React app.

Getting Started with Redux Toolkit

Step 1: Install Packages

To get started with Redux Toolkit and React-Redux packages, you can run the following command on an existing React app:

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Alternatively, install via Create React App with:

npx create-react-app my-app --template redux
Enter fullscreen mode Exit fullscreen mode

Step 2: Create & Initialize Store

Now let's create a store to hold our states. We can create a store.js file in our src folder and add the following code in it:

import { configureStore } from '@reduxjs/toolkit'

export default configureStore({
  reducer: {} //add reducers here
})
Enter fullscreen mode Exit fullscreen mode

The configureStore here replaces the original createStore from Redux. Unlike createStore, configureStore from Redux Toolkit not only creates a store, but it can also accept reducer functions as arguments and automatically sets up the Redux DevTools Extension for easy debugging.

Step 3: Provide Store in React app

Once our store is created, which we will need every component in our React app to be able to access. We can do this using the Provider from our react-redux package we installed.

In our index.js file, we import the Provider and our store.js like so:

import store from './store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)
Enter fullscreen mode Exit fullscreen mode

Step 4: Write Reducers and Actions

We can now write some reducer functions and actions for our Redux store.

In the traditional Redux, we usually write reducers and actions separately. For example, a simple reducer and action for a counter app will be written in traditional Redux like so:

Actions

// actions/index.js
export const Increase = () => ({
  type: 'INCREASE'
})

export const Decrease = () => ({
  type: 'DECREASE'
})
Enter fullscreen mode Exit fullscreen mode

Reducers

// reducers/index.js
export default (state = 0, action) => {
  switch (action.type) {
    case 'INCREASE':
      return state + 1
    case 'DECREASE':
      return state - 1
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

With Redux Toolkit, we can make the code much more concise by using createSlice. Create a counterSlice.js file in the src folder of the app. Both the reducers and actions can be written under a slice like so:

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increase: state => {
      state.value += 1
    },
    decrease: state => {
      state.value -= 1
    }
  }
})

// each case under reducers becomes an action
export const { increase, decrease } = counterSlice.actions

export default counterSlice.reducer
Enter fullscreen mode Exit fullscreen mode

As seen from the code above, defining reducers and actions become cleaner and faster in Redux Toolkit. There is no longer need to use the switch statements to manage the action with its corresponding reducer.

Another thing you may have noticed is that it seems we are now directly mutating the state's value in the reducer function instead of returning a new value to update the state. This is actually because Redux Toolkit uses the Immer library, which allows writing "mutating" logic in reducers.

For more information on how Immer works, feel free to visit its documentation here.

Step 5: Import Reducer to Store

We have exported our reducers and actions from our counterSlice.js. So let's import the reducer into our store.js.

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '.counterSlice' //import our reducer from step 4

export default configureStore({
  reducer: {
    counter: counterReducer //add our reducer from step 4
  }
})
Enter fullscreen mode Exit fullscreen mode

Step 6: Dispatch Actions from UI

As we have learned earlier, our View triggers an action to be dispatched in order to update a state. In Redux, we use store.dispatch(action) to dispatch an action.

Instead, let's use React-Redux to use the useDispatch hook to dispatch actions and useSelector to read data from the store.

Create a Counter.js file in our src folder to represent our Counter component. In this file, we will import our useDispatch and useSelector hooks from React-Redux. We will also import our actions from our counterSlice.js.

import { useSelector, useDispatch } from 'react-redux'
import { decrease, increase } from './counterSlice'
Enter fullscreen mode Exit fullscreen mode

Then, our Counter function will initialize our 2 hooks and return UI elements with our dispatch(action) triggered when clicked.

export function Counter() {
  const count = useSelector(state => state.counter.value)
  // in our slice, we provided the name property as 'counter'
  // and the initialState with a 'value' property
  // thus to read our data, we need useSelector to return the state.counter.value

  const dispatch = useDispatch()
  // gets the dispatch function to dispatch our actions

  return (
    <div>
        <button onClick={() => dispatch(increase())}>
          Increase
        </button>
        <p>{count}<p>
        <button onClick={() => dispatch(decrease())}>
          Decrease
        </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue.
It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay is the only open-source alternative currently available.

OpenReplay

Happy debugging, for modern frontend teams - Start monitoring your web app for free.

Conclusion

Redux Toolkit is a great option for both beginners and developers who wants to reduce the amount of boilerplate code in Redux. It allows us to write cleaner and more readable code while keeping the Redux flow and pattern.

Thank you for reading. I hope this article has been helpful in getting you started with understanding Redux and using Redux Toolkit in your applications. Cheers!

Latest comments (46)

Collapse
 
leszekkalibrate profile image
LeszekKalibrate

Great article, thank you.

Collapse
 
juanpumpkinpie profile image
juanpumpkinpie

🤦‍♂️ and the classic: clic pay to read something opposite of which an article is. I'm sorry but it sucks the way people try get attention on the vlog writing totally different stuff around that I didn't even read all bullshit here

Collapse
 
fullstacksk profile image
Shailendra Kumar

After reaching comment section, I have just realised that two developers also can fight to each other 😅. We should respect each other and use whatever you are comfortable in.

Collapse
 
h3li0 profile image
Helio da Silva Jr.

Thank you so much for this article. Just what I needed.
:)

Collapse
 
cfecherolle profile image
Cécile Fécherolle

Very nice explanation and example, tailored to both people not using Redux yet, or thinking about migrating to RTK :) Thanks for this article.

I have a complementary question though: how does RTK integrate with async middlewares such as Thunk? Nowadays, most React websites using Redux for state management also have async actions (API calls, mostly), which come with their own cumbersome stash of boilerplate (creating a pending/success/error action type every time, huge switch blocks in reducers, repeated testing for very similar actions, etc.)

I'm seriously thinking about migrating to RTK, but I don't quite see how this would integrate with our complex mix of async/non-async actions.

Collapse
 
markerikson profile image
Mark Erikson

RTK already enables the thunk middleware by default:

and includes a createAsyncThunk utility that abstracts the typical "dispatch actions before and after the request" pattern:

That docs page is effectively also the migration guide. Switch your store setup, then migrate one slice reducer at a time.

It also comes with a new "RTK Query" API that completely abstracts the entire data fetching and caching process, and can remove the need to write any thunks or caching reducers whatsoever if you choose to use it:

Collapse
 
cfecherolle profile image
Cécile Fécherolle

Thank you for all these details, I will check it out :D

Collapse
 
allindeveloper profile image
Uchendu Precious

Why should i install a library that's about 10mb
When i can just install redux. redux-thunk and react-redux and the size won't still be half 10mb ? plus I'll still get my work done!!

Collapse
 
phryneas profile image
Lenz Weber

It is 10kb. Including redux, reselect and immer. bundlephobia.com/package/@reduxjs/...

Collapse
 
brianmcbride profile image
Brian McBride

You can throw a digital rock and hit a library aimed to make Redux eaiser.

The basic concept of Redux is fine. It did help a lot of people learn how to stop polluting their views with business logic. But I just feel all this keeps piling up more and more and more.

If you are only in React-land. Take a look at something like Hookstate
blog.logrocket.com/simplify-react-...

There is a lot I like about Mobx, but I sure don't like the fact that I'm wrapping everything in a higher order component. Clearly there are ways you can use hooks now to tie into the component lifecycle.

If we over-simplify, all we are doing is running a dirtry check on a object, and then sending the new state update to the components that are reading those key/value pairs (ideally) through a lifecycle/api trigger.

React Context... I must be missing something. I'm basically putting a JS lib in the context (my store, reducers, etc.. ) so I can use them in child components. Yet, I literally have a module system... import { MyState } from 'my-state.ts' As long as whatever state code I use triggers the React lifecycle properly, then I'm only updating the components I need. There must be some magic with Context that I'm missing that makes it so much better than just a regular JS import.

The Hookstate lib above basically wraps your state object in a Proxy. When you get a value from it, that triggers a get accessor in the Proxy where the lib uses the React useState, useEffect (maybe some others) to do the actual component update. Attached via a subscriber to the hookstate. Honestly, even Hookstate could be replaced by RxJS pretty fact - it is the Proxy code in there that is super handy. Either you do something like that to auto-detect what key/values are being used in what components or you need to declare what key/values you want to subscribe to in your component explicitly.

Maybe it is just me. I'd rather build a function that clearly does something getUserData(userId) and that results in a state update vs dispatch({ type: STATE_ACTIONS.getUserData, payload: userId }) My method doens't need thunk, sagas, etc... Yeah, I might call it in a useCallback(), but that's ok.

Collapse
 
wplcz profile image
Wojciech Palacz

It's not really true about reducing amount of boilerplate, I went through refactor in recent projects that introduced RTK and in fact it reduces just one file from 3 that are being used: actions / reducers / selectors

Code has became much less clear and in the result readable.
Im not mentioning the case of forcing ppl to use redux thunk as it's included in RTK package, personally I do prefer Saga over Thunk.
Imho it's not that worthy as It has been described above.

Collapse
 
markerikson profile image
Mark Erikson

Can you give an example of the before and after here, and show some RTK code that you feel is "less readable"?

the case of forcing ppl to use redux thunk as it's included in RTK package

This is a misunderstanding. Nothing about RTK forces you to use thunks. You can still add the saga middleware to the store, use that, and ignore thunks entirely if you want. RTK simply adds the thunk middleware to the store by default, and provides a createAsyncThunk API to simplify the common thunk usage patterns, but you are not at all "forced" to use thunks whatsoever.

Collapse
 
wplcz profile image
Wojciech Palacz

Actually it is as RTK includes ReduxThunk by default, copied from redux official page:
It includes several utility functions that simplify the most common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire "slices" of state at once without writing any action creators or action types by hand. It also includes the most widely used Redux addons, like Redux Thunk for async logic and Reselect for writing selector functions, so that you can use them right away.

RTK is less readable as it comes with Typescript, everything combined within one slice is less readable, apologies however I can't provide you with all code samples that made me feel its not clear :D

Collapse
 
kamlekar profile image
Mr_Green

Very well explained. Thanks

Collapse
 
benjaroa profile image
benjaroa

Nice post! Can I ask you what software are you using to make that drawings? (the ones from esri). Thanks in advance!

Collapse
 
captaincodeman profile image
Simon Green • Edited

It's a pity that the solution to Redux complexity is to add yet more JS code, rather than to give it the major refactor it really needs. IMO it suffers from over-use of currying for configuration, which is "clever" but makes learning and using it way more difficult than it needs to be. The definition of actions and reducers is repetitive and complex with code for a single thing spread out in multiple locations which again, in my experience, makes it really difficult for people to learn.

Although I loved what Redux ultimately provided (predictable state), the way it went about it was so convoluted it made working with it a chore. Tools like Rematch mask some of the issues but the fundamental problems can't be solved with wallpaper.

I had a go at creating something similar to Rematch, which was based on the same premise (reversing the store definition to make things easier) but done as a complete re-write rather than a veneer on top, and aiming to provide full strict Typescript support:

captaincodeman.github.io/rdx

Collapse
 
antonmelnyk profile image
Anton Melnyk • Edited

with code for a single thing spread out in multiple locations which again, in my experience, makes it really difficult for people to learn.

And that's the whole point. Redux is about separating what happend from how that changes the state. Without separation there is no point to use Redux at all except maybe for cool dev tools.

Collapse
 
brianmcbride profile image
Brian McBride

Why do we need to use this action/payload and then switch on the action type?
We are going to write code in those switch blocks. Why are we not just writing a function: doMyAction(payload) ?

I totally agree with what you are saying. I don't see the need for Context as we have JS modules. import works really well. There are plenty of more maintainable patterns; singleton, factory, etc...

When I see the whole { action: 'ACTION', payload: somePayload } I think of the rare times I use some broadcast tooling. Event emitting can be fun, but over time it, too, ends up being complexity that is rarely needed.

I understand wanting predictable state 10000%. How my teams maintain that - unit tests. I'll stop this over-reply to emphasis that for newer developers. When it comes to your state and business logic, write unit tests. Get as close to 100% coverage as you can. I'm more lax about testing UI components (it is still good), but whatever state code you use - test the shit out of it.

PS. Simon, always love your thoughts. I remember how prolifiic and helpful you were on the Polymer slack channel. I still wait for WebComponets to just replace React components. LIT? Maybe?

Collapse
 
markerikson profile image
Mark Erikson

Why are we not just writing a function: doMyAction(payload) ?

Because "just functions" does not provide the properties needed to enable Redux's design goals, including serializing a history log of "all the things that happened in the app" and "allowing middleware to modify actions".

See the list of goals under The Tao of Redux, Part 1 - Implementation and Intent: The Intent and Design of Redux.

Thread Thread
 
brianmcbride profile image
Brian McBride

I'm not a huge fan of middleware either. It is sort of dispatch -> mystery transformation -> reducer. Entering a codebase with a lot of middleware can be super confusing.

This is not elegant, but an example myMethod(aTransformer(payload)) another developer coming in knows exactly what is going on. The downside is that you will be typing in that transformer over and over. If the middleware is fetching data, well. fetchFn(payload).then(myFunction)

I also totally get the idea of serializing events. I have built many API services that do something similar. I often use something like jsonpatch.com/ where I have a data object that I can apply, unwind, flatten, etc... It is pretty easy to have a function getPatch(prevState, nextState) then drop that in a Set/Array/Database/etc...

I'd argue that something like a JSON Patch becomes more useful anywhere. What you define as { action: "someAction", payload: "somePayload" } is not always a straight up merge. Often the payload is a single key in the state. Or there is business logic in the reducer. Thus, you need to know how the code works and if you change the the code, you break all previously stored actions if you needed to reapply them.

Of course, I'm talking about actually storing event history. In reality, how many React develpers do much more than a simple undo and even then I rarely see that. What I do see is that developers use the actions in Redux tooling to see if their event fired and what side effects might have happened. And right there I would say build that all in a Jest test and check your results there with assertions. Like, actually develop in your test runner. Then you have test coverage and you'll know if your state breaks with some code/dep update.

Collapse
 
markerikson profile image
Mark Erikson

What specific concerns do you have?

The only uses of "currying" I can think of are writing middleware, which is a very rare use case, and the connect API, which has basically been replaced by useSelector.

Looking at your linked lib, you show this example:

import { createModel } from '@captaincodeman/rdx'

export const counter = createModel({
  state: 0,
  reducers: {
    add(state, value: number) {
      return state + number
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

RTK's createSlice is effectively identical to that:

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    add(state, action) {
      return state + action.payload;
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

and RTK is written in TS and designed for a great TS usage experience:

redux.js.org/tutorials/typescript-...

The only immediate difference I see is that RTK's action creators and reducers are still standalone functions, rather than being attached to a store instance.

Collapse
 
narven profile image
Pedro Luz

Redux Toolkit is really nice evolution from Redux.
Just for the amount of code reduced its worth the refactoring.

Collapse
 
captaincodeman profile image
Simon Green

I haven't used it, but I'm assuming it reduces the amount of code you have to wrote, but increases the amount of code that is bundled in the app, is that correct?

Collapse
 
andrewbaisden profile image
Andrew Baisden

It definitely reduces the amount of code I know that much from experience. The code is so much simpler and easier to read and its not the default way for building Redux apps.

Collapse
 
markerikson profile image
Mark Erikson

RTK does add some code to the bundle size, yes, but:

  • The biggest added size comes from the Immer immutable update library. We've already been recommending use of Immer for immutable updates for a long time, so lots of people were using that anyway
  • The actual logic added by RTK is fairly minimal
  • The amount of app code that goes away more than pays for the size of the RTK methods
Collapse
 
dstran profile image
David Tran

Thank you for the article! This feels a lot like vuex which makes writing code much cleaner.

Collapse
 
burhan5246 profile image
burhan5246

How to manage async operation in redux toolkit

Collapse
 
hipertracker profile image
Jaroslaw Zabiello

Overmind is much, much better overmindjs.org/