DEV Community

loading...
Cover image for Get to know Redux in 2021

Get to know Redux in 2021

Semir Teskeredzic
・6 min read

Redux is something you really need to know if you are going to do anything professionally with JS and especially React. For some time it seemed quite complex with a lot of boilerplate so I mostly used MobX and more recently React context.
However, my curiosity got better of me and I had to dig a bit deeper to comprehend the great Redux. In this post I will try to simplify basic concepts of how Redux works so you can try and not just build but also comprehend a React-Redux app.

What is Redux?

"Redux is a predictable state container for JavaScript apps." (https://redux.js.org/introduction/getting-started). It is a place that manages the state and makes changes according to the provided actions.

What is it for?

For use cases when you need to have data available across the application i.e. when passing data through props is not possible.

Why is it powerful?

Redux is highly predictable which makes debugging much easier since you know what is happening where. It is also scalable so it is a good fit for production grade apps.

Brief overview

Let's say you're making an app that increments the count. This app has:

  • Count value,
  • Increment button,
  • Decrement button,
  • Change with value,

What is then happening?

When you want to increment a count, you dispatch an action. This action then through special function called reducer takes the previous state, increments it and returns it. Component that listens through Selector re-renders on change of state.

Let's go to the code

In order to create the "Counter" app with React and Redux, we need to add following packages to your React app (I will assume you know how to create a basic Create React App):

yarn add @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Now the first thing we will do is to create a Store and provide it to the entry point of your App, in this case it is Index.js

/src/app/store.js

import { configureStore } from "@reduxjs/toolkit";

export const Store = configureStore({
});
Enter fullscreen mode Exit fullscreen mode

Here we are using configureStore from Redux toolkit which is a function that requires passing a reducer. We will get back to it in a second.

/index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";

import App from "./App";
import { Store } from "./app/store";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <Provider store={Store}>
      <App />
    </Provider>
  </StrictMode>,
  rootElement
);
Enter fullscreen mode Exit fullscreen mode

Here we are using Provider to provide our Redux store to all wrapped Components.
Believe it or not, we are half way there!

Next, we need to populate the core of our Redux logic and that is the Slice. You can think of Slice as a collection of Redux reducer logic & actions for a single feature in the app.
(in a blogging app there would be separate Slices for users, posts, comments etc.).
Our Slice will contain:

  • Initial value
  • Increment logic
  • Decrement logic
  • Change by value logic

Let's go:

/src/features/counterSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const Slice = createSlice({
  name: "counter",
  initialState: {

  },
  reducers: {

  }
});
Enter fullscreen mode Exit fullscreen mode

First we have a named import for createSlice from toolkit. In this function we are giving it a name, setting initial state, and providing logic as reducers.

/src/features/counterSlice.js

...
export const Slice = createSlice({
  name: "counter",
  initialState: {
    value: 0
  },
...
Enter fullscreen mode Exit fullscreen mode

Here we set the initial state to 0, every time we refresh our application it will be defaulted to 0. More likely scenario here would be fetching the data from external source via async function. We won't be covering that here but you can read more about async logic with Thunks.

In our reducers object we will have increment, decrement, and changeByValue:

/src/features/counterSlice.js

...
reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    changeByValue: (state, action) => {
      state.value += action.payload;
    }
  }
...
Enter fullscreen mode Exit fullscreen mode

Now it starts to make sense. When we dispatch an action from our component we are referencing one of these in the reducers object. Reducer is acting as an "event listener" that handles events based on received action type while Dispatching actions is "triggering events".
With increment and decrement we are updating state value, while changeByValue takes action payload to determine the exact value of that update.
Only thing left to do in the slice is to export Actions, State reducer, and state value. Here is a complete file

/src/features/counterSlice.js

import { createSlice } from "@reduxjs/toolkit";

export const Slice = createSlice({
  name: "counter",
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    changeByValue: (state, action) => {
      state.value += action.payload;
    }
  }
});
export const selectCount = (state) => state.counter.value;

export const { increment, decrement, changeByValue } = Slice.actions;
export default Slice.reducer;
Enter fullscreen mode Exit fullscreen mode

Important note here is that Reducers are not allowed to modify existing state. They have to make immutable updates which basically means copying the state and modifying that copy. Here createSlice() does the heavy-lifting for us and creates immutable updates, so as long you are inside createSlice() you are good with immutability rule 👌

We now need to update our store with reducers we made:

/src/app/store.js

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counterSlice";

export const Store = configureStore({
  reducer: {
    counter: counterReducer
  }
});
Enter fullscreen mode Exit fullscreen mode

The only thing left to do is to create a component that will be the UI for our app:

/src/features/Counter.js

import React, { useState } from "react";

const Counter = () => {
return (
    <>
      <h1>Counter app</h1>
      <p>Count: </p>
      <button>Increment</button>
      <button>Decrement</button>
      <button>
        Change by Value
      </button>
      <input/>
    </>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

We are starting from this base. We will need a way to:

  • Show current count status
  • Increment on click of button
  • Decrement on click of button
  • Input value for change
  • Apply value to the count

We have already exported the current state from the Slice like this:

/src/features/counterSlice.js

export const selectCount = (state) => state.counter.value;
Enter fullscreen mode Exit fullscreen mode

We can now use this to show current value using useSelector()

/src/features/Counter.js

...
import { useSelector } from "react-redux";
import { selectCount } from "./counterSlice";

const Counter = () => {
  const count = useSelector(selectCount);

return (
    <>
      ...
      <p>Count: {count}</p>
      ...
    </>
  );
...
Enter fullscreen mode Exit fullscreen mode

As we mentioned earlier, we will use useDispatch() to dispatch actions we need -> increment, decrement, changeByValue:

/src/features/Counter.js

...
import { useDispatch, useSelector } from "react-redux";
import {
  increment,
  decrement,
  changeByValue,
  selectCount
} from "./counterSlice";

const Counter = () => {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();

  return (
    <>
      ...
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(changeByValue(value))}>
        Change by Value
      </button>
      ...
    </>
  );
};
...
Enter fullscreen mode Exit fullscreen mode

Increment and Decrement are pretty much self-explanatory, but with changeByValue we have a variable value that we need to define in order to send it as a payload. We will use React local state for this with onChange and handleChange() to set this value properly. With those additions we have a complete component:

/src/features/Counter.js

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
  increment,
  decrement,
  changeByValue,
  selectCount
} from "./counterSlice";

const Counter = () => {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [value, setValue] = useState();

  const handleChange = (e) => {
    const num = parseInt(e.target.value);
    setValue(num);
  };

  return (
    <>
      <h1>Counter app</h1>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(changeByValue(value))}>
        Change by Value
      </button>
      <input onChange={(e) => handleChange(e)} />
    </>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

With this addition, we have a working React Redux app. Congrats! You can install Redux dev tools to your browser to see what is exactly happening and how actions mutate the state.

Recap

After seeing how everything connects together, here is the recap of the update cycle that happens when the user clicks a button to increment/decrement count:

  • User clicks a button
  • App dispatches an action to Redux store
  • Store runs reducer function with previous state and current action after which it saves return value as the new state
  • Store notifies all subscribed parts of the UI
  • Each UI component that needs data checks if it is what it needs
  • Each UI component that has its data changed forces re-render with the new data

Diving into Redux might seem daunting but once you get hang of basic principles it becomes a powerful weapon in your coding arsenal.

Thank you for reading,

'Take every chance to learn something new'

Discussion (9)

Collapse
webduvet profile image
webduvet • Edited

I think redux-toolkit is just plain ugly. The only nice part is that integrates with react hooks. It is just too little for platform "independent" library.
it states solving 3 problems:

1.  "Configuring a Redux store is too complicated"
2.  "I have to add a lot of packages to get Redux to do anything useful"
3.  "Redux requires too much boilerplate code"
Enter fullscreen mode Exit fullscreen mode

And I don't thing they were problems at first place and even if they were (for some) it does not solve it.

  1. not it is not complicated at all. It's rather plain simple. toolkit is IMO more comlicated as it tries hiding things.
  2. no, redux is the only required package and if you work with react there one more - react-redux. anything else is just app developer decision.
  3. no, it does not. it's about the same as with toolkit.
Collapse
markerikson profile image
Mark Erikson

You've complained in multiple threads that RTK is "ugly". What, specifically, do you think is "ugly" about it?

Also, RTK does solve all three of those issues:

  • Creating a Redux store with correct defaults is now a single function call with one required argument, instead of 30-40 lines with a bunch of conditional logic to set up applyMiddleware, check for window.__DEVTOOLS_EXTENSION__, compose() them together, etc.
  • RTK depends on the standard Redux packages like redux-thunk, immer, and reselect, so you only have to add @reduxjs/toolkit yourself.
  • It does eliminate all the boilerplate code. No more nested spread operators, no writing action types or action creators by hand, and APIs like createAsyncThunk and createEntityAdapter for simplifying standard use cases.

If you don't like Immer's "magic", that's a valid opinion and up to you. But as the primary Redux maintainer, I've seen all the problems and pain points people have had with Redux over the years. RTK does solve those problems, and the huge amount of positive feedback we've gotten about RTK reflects that.

Collapse
webduvet profile image
webduvet

It's my right to complain :) BTW thumbs up for the good work. I think redux was bulls eye and it changed FE application development forever. However I have my opinions about opinionated toolset.

  • if you have 30-40 lines to setup your store then you probably require something more than default automatic configuration. Besides that, It is not something I do every day or every week. not even every month. And I always can just copy that from previous project. I have full overview and control and I like to keep thinks simple.
  • so if I use toolkit as opposed to plain redux I have redux-thunk, immer, reselect and redux-toolkit in my dependency list. I find immer on verge of hating. to me it is not readable enough. I usually use plain js or ramda. Immer just hides things assuming that is what people want. Helping by hiding is not great approach.
  • again I would happily trade few extra key strokes any day in exchange for better readability and better control.

If I compare my reducers and selectors against something written using redux-toolkit I fail to see any excessive what you call boilerplate code.

Examples of things I do not like:

I do not like action creators coupled with reducer. It is just a function and You could hardly find anything simpler than that. redux-toolkit takes that simplicity away.
Why do you see the need for a factory method for a function?

I do not like the createAsyndThunk. redux-thunk is by definition async. I believe that catering for developers who do not
understand 7 lines of code (redux-thunk) by creating convoluted API for redux-thunk is just too much.

Thread Thread
markerikson profile image
Mark Erikson

Agreed that store setup is typically a once-per-project kind of thing. But, it's something you have to do for every project, and being able to do that in one function call, and get correct defaults out of the box (including dev mode checks that catch common errors) is a big improvement.

I'm really not understanding what you don't like about Immer. For comparison:

// hand-written immutable update
function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }
}

// one-liner with Immer
function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}
Enter fullscreen mode Exit fullscreen mode

Writing reducers with Immer results in much less code, and much clearer intent as to how you're actually trying to update state. Also, hand-written immutable updates are very prone to errors, and accidental mutations have always been the #1 cause of bugs with Redux. Immer eliminates those completely.

So, between making the code shorter and easier to read, and eliminating accidental mutations, Immer is a huge improvement for Redux users.

We have always encouraged the user of action creators as a standard practice with Redux, and I wrote a separate post on "why use action creators?" several years ago. They provide a consistent interface for dispatching actions, and also encapsulate any logic needed to set up the action object (such as generating unique IDs or formatting parameters). With RTK, you get them for free anyway, so there's no reason not to use them.

redux-thunk is by definition async.

This is incorrect. A thunk may contain any logic, sync or async, and you can still write them by hand. However, we have always encouraged the "dispatch pending/fulfilled/rejected actions" pattern, and that requires additional work: defining the action types and action creators for all three cases, and writing the logic to dispatch those actions at the right times with the right contents. So, it's a lot more than "7 lines" for each thunk if you're writing all those action creators and action types by hand. createAsyncThunk does all that for you.

Thread Thread
webduvet profile image
webduvet • Edited

1.) purposfully written example. It is rather unusual to have action targeting four levels. You could run into few worse problems than verbose code. like change in API contract if that is what defines the shape of your state object.
But if I had to I would use my favourit little functional library and it would look e.g. like this

...
return mergeDeepRight(state, { first: { second: { [action.payload.someId]: { fourth: 'hello from here'} }}})
Enter fullscreen mode Exit fullscreen mode

as opposed to:

function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}
Enter fullscreen mode Exit fullscreen mode

the beauty of the first version is that it returns the new state object. your example does not return anything and relies on immer's internal magic to figure it out
That impacts your tests and the general readability of your code.

2.) redux-thunk - what I mean by definition - perhaps wrong wording - you define the function which is executed in the middleware and returned. whether the function is async should be of no concern to redux or toolkit
so if you define function which returns promise then you know what you are doing. I can't see how is this difficult. Why does this justify a wrapper which is 50 times bigger than redux-thunk itself?

defining the action types and action creators for all three cases, and writing the logic to dispatch those actions at the right times with the right contents. So, it's a lot more than "7 lines" for each thunk

I meant redux-thunk logic is in 7 lines. however dispatching right actions with right content at the right time - you still need to take care of that whether you use redux-thunk, toolkit or your own middleware.
I only can speak for myself when I say I've never seen the above mentioned three problems which toolkit is trying to address as problems at the first place. It is maybe be because I've been already addressing them unconsciously similar way as I address similar problems outside react-redux world. But for me and like minded developers it might feel like toolkit is being forced down the thought by statements like

The Redux Toolkit package is intended to be the standard way to write Redux logic.

Thread Thread
markerikson profile image
Mark Erikson • Edited

FWIW, I can tell you I have seen all of these problems pop up, thousands of times over the years. I wouldn't have created RTK if these problems didn't exist. (and that includes seeing some very deeply nested immutable update logic - that example is fictional, but I've seen multiple cases at least that long in real production code.)

And yes, we are telling people that RTK is the right way to use Redux at this point, because it solves all of these problems, and the highly positive feedback we've gotten reflects that.

Just a couple days ago I got this DM:

Hello! I'm a software engineer who's been using Redux almost exclusively for state management for a few years now, and I wanted to say that Redux Toolkit is amazing. I've tried a few other ways to manage state, but I just love the principles behind Redux and kept coming back to it. The one thing I couldn't stand was the boilerplate at first, and I actually tried creating my own utilities to simplify it as much as possible. A couple years ago, I found immer and made my own "reducer producer" that used immer to make immutable reducers while writing simpler mutative code. Soon after, I found Redux Toolkit's createReducer which had practically the exact same API as my own and was so happy. More recently, I have been using my own utility called "geese", where I: 1. Define a "goose" (playing off of the "ducks" pattern) with an action name, creator, and handler (reducer) 2. Combine them into a map typed as "geese" 3. Create the reducer and action map from the geese using a utility function After a bit more simplification, I arrived at a final API and was in the middle of implementing it... When I found "createSlice" and realized it's practically identical. In short, I just wanted to say that Redux Toolkit is amazing and basically works exactly the way I would expect an ideal boilerplate-reducing Redux utility to work.

Some other similar responses:

1)

it’s hard to overstate how much I’m enjoying getting to migrate things to RTK. The patterns it uses / encourages just make things to much easier. Have been working on migration PRs to show our devs how to do it and it’s already made me bummed to look at the old code.

2)

BTW, we just switched from context and hooks over to RTK on one of our production application's frontends. That thing processes a little over $1B/year. Fantastic stuff in the toolkit. The RTK is the polish that helped me convince the rest of the teams to buy into the refactor. I also did a boilerplate analysis for that refactor and it's actually LESS boilerplate to use the RTK than it is to use KCD's recommended dispatch pattern in contexts. Not to mention how much easier it is to process data.

3)

I'm a huge fan of how effortless it is to create airtight typed Redux setups now with Redux Toolkit. Extremely appreciative of the incredible work you and the rest of the good folks working on Redux have given the community.

I'm not saying that you personally must like RTK or use it. I am saying that it does solve the problems that most Redux users have experienced, and that almost everyone who has used RTK loves it.

Thread Thread
webduvet profile image
webduvet

Well, I'm not arguing that many find it easier with redux-toolkit. You(as toulkit devs) obviously have vision and aim which is great. And you have a feedback from thousands of users and if this is how the community wants to drive it so be it. :)
I only expressed my own experience and my own opinion. Thanks for the discussion anyway, All the best!

Collapse
semirteskeredzic profile image
Semir Teskeredzic Author

Agreed, it does tend to use more "magic" approach, I can't see anything wrong or more complex if one wants to use Redux without toolkit package. Thanks for the insight.

Collapse
abror1997 profile image
Forem Open with the Forem app