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!

Oldest comments (46)

Collapse
 
emmanuelchucks profile image
Emmanuel Chucks

I used to hate Redux, until Toolkit came out

Collapse
 
mdxprograms profile image
Josh Waller

this 1000000 percent

Collapse
 
srmagura profile image
Sam Magura

Redux Toolkit seems super opinionated to me. Opinionated libraries are fine but I think RTK takes it a bit too far. Redux has a very simple API and I feel like RTK introduces a lot of new concepts that could overwhelm new users. I'm sticking to typesafe-actions and Immer for now but maybe someone can convince me otherwise 😆

Collapse
 
stevereid profile image
Steve Reid

It didn’t overwhelm me at all when I 1st got into it, quite the opposite in fact. Plus RTK uses Immer under the hood. 👍🏾

Collapse
 
markerikson profile image
Mark Erikson • Edited

Which "opinions" are you concerned about?

The only real complaint we've gotten is actually that RTK's use of Immer is not configurable, and that's specifically because Immer makes your reducer logic much simpler and eliminates accidental mutations, which have always been the #1 type of bug in Redux apps. So, that particular "opinion" is non-negotiable. Other than that, all that RTK does is take common patterns that everyone was already doing anyway, and simplifies them.

Beyond that, RTK is already written in TypeScript, and we've designed its API to minimize the amount of types you have to declare. Example:

// Define a type for the slice state
interface CounterState {
  value: number
}

// Define the initial state using that type
const initialState: CounterState = {
  value: 0
}

export const counterSlice = createSlice({
  name: 'counter',
  // `createSlice` will infer the state type from the `initialState` argument
  initialState,
  reducers: {
    increment: state => {
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

All you need to declare is the type for this slice's state, and the payload type for each action, and RTK will infer the rest for you. See redux.js.org/tutorials/typescript-... for a summary of recommended TS setup.

Collapse
 
srmagura profile image
Sam Magura

It seems like I may be in the minority here, which is fine 😄. I also don't want to detract from your work on the library!

I suppose the three parts of RTK that I consider very opinionated are:

  • createAsyncThunk — this seems to be specifying a side effect model for Redux. I've only ever used Redux Saga for this so I don't really understand thunks. Maybe if I used them I would understand why this was included in RTK.
  • createEntityAdapter — I always thought the Redux philosophy was to design action types, reducers, and selectors specific to each entity since different entities usually have different operations and filtering logic. I can see createEntityAdapter being useful if you have a ton of very straightforward CRUD in your app.
  • RTK Query — I think Redux Toolkit should focus on helping you build actions, reducers, and selectors (core Redux features) without going too far beyond that. RTK Query is a library for querying & caching server state, neither of which are features of Redux (even if many applications use Redux like this). My initial reaction to RTK Query would have been more positive if it was a separate package.

Just my two cents. I know these are all optional features and that tree shaking will probably remove them if you don't use them.

Thread Thread
 
markerikson profile image
Mark Erikson

Responding to each of those items:

createAsyncThunk

Yes, we do specifically recommend thunks as the default approach for side effects in Redux. That does not mean it's the only approach you can use. All the existing side effects libs (sagas, observables, 5 million others) still work fine. But the most common side-effect-y thing people need to do is basic AJAX fetching. Thunks are the simplest approach to doing that, and they have also always been the most commonly used approach.

FWIW, we're actually trying to nudge people away from using sagas as a default approach, especially for just data fetching. Don't get me wrong, sagas are a great power tool for complex async workflows that involve debouncing or "background thread"-type behavior. But they're overkill for basic data fetching, and one of the contributing reasons why people have long complained about the "boilerplate" associated with Redux. They shouldn't be the first tool people reach for when they want to fetch data, and they shouldn't be something that's "required to use with Redux".

So, since people were already doing this anyway, and the docs have always taught this "promise lifecycle actions" pattern from the beginning, it made sense to add createAsyncThunk to simplify the code that people were already writing.

References:

createEntityAdapter

We have always recommend storing items in a normalized lookup table. In fact, I specifically wrote the Structuring Reducers: Normalizing State Shape docs page back in 2016 to officially start teaching that concept. But, we never had any built-in APIs that helped with any of that process. For any collection of items, you are very likely to do a number of CRUD-y type operations.

So, createEntityAdapter provides the reducers that implement those operations, and it's up to you to decide when and how it makes sense to run those reducers, and in response to what actions. They can be used as complete "case reducers", or as "mutating helper functions" inside of other Immer-powered reducers. Again, we just looked at "what are things people commonly have to write on their own?", and provided some generic implementations. You're free to use them, or not. All these APIs are pick-and-choose.

RTK Query

Aas mentioned with thunks, users have always used Redux stores as a server state cache. It's just that Redux never included anything built in to help with that process. So, you inevitably ended up having to write all your own logic to manage loading state, make the requests, define the actions, dispatch the actions, save the data, handle errors, etc. The growth of React Query and Apollo have shown that the community has become much more interested in "server state caching" than "state management" in many cases. Redux makes a perfectly fine place to cache data, but writing the code to do that yourself is a pain.

So, once again, we built a set of APIs that just simplifies what everyone else has been doing by hand already, and provides a best-in-class API for dealing with server state caching.

RTK Query is built on top of createAsyncThunk and the other APIs in Redux Toolkit. As the RTK tagline says, it's "The official, opinionated, batteries-included toolset for efficient Redux development". Yes, RTK Query is effectively a second library, but it it's also very reasonable to include it in the actual RTK package. For one thing, some enterprises have difficult approval processes to go through to add additional libraries. By including the RTK Query APIs directly in the RTK package, you get them "for free" just by bumping to RTK 1.6+.

And yes, not only do the RTK APIs all tree-shake so you only pay for what you use, RTK Query itself is an entirely separate entry point, so none of it gets included in the app unless you specifically import it.

As you can see, all of these APIs have a common theme:

  • These are things people were always doing with Redux, and have been taught in our docs
  • Because Redux didn't include anything built-in for this purpose, people were having to write this code by hand, or create their own abstractions
  • So we created standardized implementations of all these concepts that people could choose to use if they want to, so that people don't have to write any of this code by hand in the future.

Hopefully that helps clarify why these APIs exist :)

Thread Thread
 
srmagura profile image
Sam Magura

Awesome response Mark. That really clarifies why those APIs were included.

You've really got me thinking about this and I'm planning to write a post about my thoughts on RTK, both the pros and the cons. I will link it here when/if it gets completed.

Thread Thread
 
markerikson profile image
Mark Erikson

Sure, please do! I'd also suggest reading through my Redux Toolkit 1.0 post, which goes through the entire history and background of why RTK was created, what problems it tries to solve, the "vision" for the library, and the evolution of its development and APIs up to the 1.0 release.

Obviously I'm biased, but the only meaningful tradeoffs I personally see in using RTK over "vanilla" Redux are:

  • Byte size for the extra functions and dependencies
  • Immer is an abstraction layer, and it's possible for someone to look at some Immer-powered reducers and not realize that there's "magic" inside and that they still have to do updates immutably in the end
  • Because Immer wraps the draft state in a Proxy, debugging can be harder because logging or inspecting the draft shows the Proxy instead and the contents are very hard to read

On the flip side of those:

So yeah, RTK certainly isn't perfect, but I can only think of a couple very minor concerns, and those are in turn far outweighed by all the benefits: standardized APIs, smaller app code, and prevention of common mistakes.

Thread Thread
 
srmagura profile image
Sam Magura

My post is here.

I hope I succeeded in providing an accurate and balanced review of RTK. At the end of the day, it's all just my opinion.

Collapse
 
antonmelnyk profile image
Anton Melnyk • Edited

You're not alone on that, man. While forced Toolkit and opinionated decisions are understandable and they do cover probably 90% of developers that use Redux, I as well think it's too opinionated.

One of advantages of Redux is very simple API surface that is easy to reason about. Boilerplate code is nothing in big projects (for which Redux is suited), if you think Redux has a lot of boilerplate, that means your project is too small to benefit from 1 action - many reducers pattern and Redux in general.

I personally don't like Immer as well - whole point of Redux is functional pattern, focused around immutability and pure functions (reducers). Instead embracing pure functions, Toolkit tried to hide purity by introducing magic mutations by Immer (yes, I understand they're not mutations under the hood, but it doesn't matter - Immer and code that LOOKS like mutation does not compose). For me Redux looks much better with functional libraries like Ramda.

And lastly, in my opinion slices force connection between actions and reducers - while in idiomatic Redux there is none. The whole point of Redux is to separate one from another, not connect them to "slice".

Collapse
 
markerikson profile image
Mark Erikson

I think this is a misunderstanding or mislabeling of "pure functions".

Immer-powered reducers are pure functions. They produce the new state immutably, and they do not actually mutate any values outside the function. It's just that the immutable updates are done via mutating syntax. (And fwiw, Dan Abramov has pointed out just how useful something like Immer is as well.)

I would also argue that createSlice in particular mostly shrinks the Redux API surface, because you're no longer having to write action creators and action types yourself - those now become an implementation detail that you no longer need to worry about.

if you think Redux has a lot of boilerplate, that means your project is too small to benefit from 1 action - many reducers pattern and Redux in general.

as Redux maintainer, I would disagree with this. I've seen thousands of Redux projects of all sizes, and trust me, the "boilerplate" complaints were real. Fortunately, RTK solves them.

Collapse
 
milandry profile image
Michael Landry • Edited

We adopted Redux when it first came out. While you are correct in that the API is very simple, and the strategy of Redux is simple, implementing state with Redux is anything but. I recall making several passes through the tutorials to grok the what is required to implement Redux in an app. Configuring the store, configuring the 'mother reducer', learning about HOCs, considering implications on the rest of the architecture, implementing selectors, unit testing, state "slices" and what that could or could not mean, leveraging the 'backloors' (ie 'ownProps') it was really overwhelming to me. After taking a hiatus from React for Angular, I found myself architecting a new React app, encountering RTK for the first time.I got to say that my impression was that it does a really good job in building the right abstractions for an engineer just trying to architect an app and manage its state. Granted that I would not really be qualified as a 'new user', I thought it was a step in the right direction for the purposes of Redux adoption. I would argue that the new concepts of RTK are also present in vanilla Redux, but in a more transparent way, IE the considerations I had just mentioned. Thinking about it, RTK is really just an abstraction that wraps vanilla Redux, essentially putting it on rails. With any rail system, you trade in some flexibility for safety in terms of removing chances for misuse. In that respect, RTK adoption should probably considered on a case by case basis. my 2 cents 😊.

Collapse
 
villar74 profile image
Maksim

Rematch is better in my opinion

Collapse
 
sendypw profile image
Sendy Putra

me too

Collapse
 
kapilpatel profile image
Kapil Patel

I am not a React developer still I could benefit from this post, thanks for making it simpler to understand. Now I am more interested in React

Collapse
 
deepanshurohilla profile image
DeepanshuRohilla

Okay but where does that headline explained ?
How redux is dead ?

Collapse
 
jenueldev profile image
Jenuel Oras Ganawed

try pinia or vuex

Collapse
 
hipertracker profile image
Jaroslaw Zabiello

Overmind is much, much better overmindjs.org/

Collapse
 
burhan5246 profile image
burhan5246

How to manage async operation in redux toolkit

Collapse
 
dstran profile image
David Tran

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

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
 
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
 
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.