Just Redux: The Complete Guide
Tags: react, javascript, webdev, frontend
Published: 2021-09-23 15:13:12

Cover image by [Josh Weirick](https://unsplash.com/@weirick) on [Unsplash](https://unsplash.com/photos/bLTd_HYFHGg).

Hey! :wave:

If you're a Front-end developer or aspiring to become one, I am sure you might have come across Redux by now. 

Maybe you probably know what Redux is, maybe you don't. Maybe you have been using it for quite a while but you don't fully understand it. You start a new project and then just copy a bunch of stuff from somewhere and just get it set up. TBH, I've done this before. I had slight idea of everything and what should be in place for Redux to work. That has worked for me until now, but sometimes I'd run into issues that would require a little more knowledge.

🙇‍♂️ Therefore, I decided to study the Redux API. I watched a bunch of videos online and read the docs. Along with that I wrote this article.

🤯 To my surprise, I found that 80-90% of the things we do in Redux is just plain JS. It's just objects and functions. If it feels complicated, you might wanna go back to JS basics. But if you're confident on the JS part, Redux won't be tough.

⚠️ Before starting I would like to mention that this article covers Redux only. It does not talk about React or any other framework or their interactions with Redux.

👨‍💻 To make the most out of this article, you can code along. I have added snippets for everything we're going to discuss.

<br>
<br>
## 🤨 What is Redux?

Well, if you clicked on this article, I am pretty sure you already know the answer. But just for the sake of answering the question, let's do it.

Redux is a state management library. It stores the state of your app and provides methods to interact with that state. It can be used with any framework like React, Angular, Vue etc.

**Installation**
```bash
npm install redux
```

For this article we'd only need to install `redux`, nothing else.

---

Redux API surface comes with only 5 methods.

![Screenshot 2021-09-22 at 4.55.33 PM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2x8d1a346qmckc9iu16.png)

We'll study each one of these in detail.

<br>
<br>
## 👉 `compose`

This method doesn't even have anything to do with Redux. The purpose of this method is to bundle multiple functions into one.

Let's say we have 3 mathematical functions: `half`, `square` and `double`.

If we want to apply all three operations in order we'd need to do something like this:

```js
const double = (num) => num * 2;
const square = (num) => num * num;
const half = (num) => num / 2;

const halfSquareDouble = (num) => half(square(double(num)));

console.log(halfSquareDouble(2)); // 8
```

But we can achieve the same thing in a much cleaner way using `compose`:

```js
import { compose } from "redux";

const double = (num) => num * 2;
const square = (num) => num * num;
const half = (num) => num / 2;

const halfSquareDouble = compose(half, square, double);

console.log(halfSquareDouble(2)); // 8
```

`compose` will combine all our functions into a single function.

🧪 **Note:** `compose` will start picking up functions from the right end. That means if the order was `compose(half, double, square)` then the result for the same call would have been 4.

<br>
<br>
## 👉 `createStore`

This methods creates the Redux store. It takes one mandatory argument `reducer`, and two optional arguments - `preloadedState`(also know as `initialState`) and `enhancer`.

So, what is a Reducer? In simple terms, Reducer is just a pure function which takes two arguments - `state` and `action` and returns one value which is the new `state`.

![Screenshot 2021-09-23 at 6.34.49 PM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g8oc6punguttqb9ahv0l.png)
 
Understand it like this, there is a perfect world/simulation which is in some `state` X. Something happens; some `action` is taken. We don't care where the action took place or who was responsible for it. All we know that something happened and that *might* change the state of our world. It is the reducers' job to figure out the new `state` Y. 
 
```js
const reducer = (state, action) => {
  return state
}
```

This is the simplest reducer you can create.

When we call `createStore` method, it returns an object.

```js
import { createStore } from 'redux'

const reducer = (state, action) => {
  return state
}

const initialState = { value: 0 }

const store = createStore(reducer, initialState)
```

That object has 4 methods:

1️⃣ `getState`: This method is used to get the state of your app.

```js
console.log(store.getState()) // { value: 0 } 
```

2️⃣ `subscribe`: This method is used for subscribing to the changes on our store. Pass a function to this method and it will get called anytime state changes.

```js
store.subscribe(() => console.log("State changed!"))
```

3️⃣ `dispatch`: This method is used for dispatching actions. Actions go inside reducers with the current state of your app and *might* update the state.

🏌️‍♂️ We've introduced one more term here - `action`, so let's talk about it.

If you remember reducer takes action to update the state. It's the action that tells the reducer that something just happened. It can be user clicking on a button, user logging in, user adding a product, etc. Anything that is meant to change the state of our app is an action.

Of course we've full control over them. We're the ones defining them. How do create them? Well, there's a specific style you should follow.

```js
const incrementAction = {
  type: 'INCREMENT'
}
```

Actions are basically objects, that have a `type` key. That's it. It can have additional keys too, but `type` is mandatory.

Let's refactor our reducer now to make use of this action.

```js
const reducer = (state = initialState, action) => {
  if (action.type === 'INCREMENT') {
    return { value: state.value + 1 }
  }
  
  return state
} 
```

On line 1, we've added `intialState` as a default argument. By doing this we can remove it from the `createStore()` call. This is actually a best practice.

On line 2, we're checking if the action that we received is of type `INCREMENT`.

On line 3, we're preparing our new state. This is important. **Never modify your state directly. Always return a newly created object.** If you don't do so, the reference to the state object won't change, and your app wouldn't get notified of the changes.
```js
state.value++ // 🙅‍♂️ DON'T DO THIS
return { value: state.value + 1 } // 👍 WORKS FINE
```

On line 4, we finally return our old state, in case we didn't find a matching action. This is important too. **Your reducer should always return a state**.

Now, that our reducer is updated, let's dispatch an action.

```js
import { createStore } from "redux";

const initialState = { value: 0 };

const incrementAction = {
  type: "INCREMENT"
};

const reducer = (state = initialState, action) => {
  if (action.type === "INCREMENT") {
    return { value: state.value + 1 };
  }

  return state;
};

const store = createStore(reducer);

console.log(store.getState()); // { value: 0 }

store.dispatch(incrementAction);

console.log(store.getState()); // { value: 1 }
```  

What if we want to increment by 5? I cannot do that right now. But if we see carefully, all we have written up until now is basic JavaScript. Stuff that you probably know. We can extend our code a bit and achieve our goal.

Remember action can have additional keys? We'll create one more action.

```js
import { createStore } from "redux";

const initialState = { value: 0 };

const incrementAction = {
  type: "INCREMENT"
};

const addAction = {
  type: "ADD",
  payload: 5,
}

const reducer = (state = initialState, action) => {
  if (action.type === "INCREMENT") {
    return { value: state.value + 1 };
  }

  if (action.type === "ADD") {
    return { value: state.value + action.payload }
  }

  return state;
};

const store = createStore(reducer);

store.dispatch(addAction)

console.log(store.getState()) // { value: 5 }
```

Okay! So far so good. But 5 is not enough, let's create one for 10 too, and then one for 100 too? Feels stupid! We cannot cover every number out there.

Okay! What if we do something like this?

```js
store.dispatch({ type: "ADD", payload: 5 })
store.dispatch({ type: "ADD", payload: 10 })
store.dispatch({ type: "ADD", payload: 100 })
```

Yes! this gets the job done but this is not scalable. If later we decide to call it `INCREASE_BY` instead of `ADD`, then we'll have to update it everywhere. Also, there's a chance that we might make a type and end up writing `INCRAESE_BY`. Good luck finding that typo! 😬

There's an elegant way to solve this using Action Creators.

🤖 Action Creators are just functions that create actions for you. 

```js
const add = (number) => {
  return {
    type: "ADD",
    payload: number
  }
}

store.dispatch(add(5))
store.dispatch(add(10))
store.dispatch(add(100))
```
We created a function `add` that returns action object. We can call it anywhere and it will create an action object for us.

This solution is much cleaner and it is widely used.

Our updated code now looks like this:
```js
import { createStore } from "redux";

const initialState = { value: 0 };

// constants
const INCREMENT = "INCREMENT";
const ADD = "ADD";

// action creators
const increment = () => ({ type: INCREMENT });
const add = (number) => ({ type: ADD, payload: number });

const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { value: state.value + 1 };
  }
  if (action.type === ADD) {
    return { value: state.value + action.payload };
  }
  return state;
};

const store = createStore(reducer);

console.log(store.getState()); // { value: 0 }

store.dispatch(increment());
store.dispatch(add(2));

console.log(store.getState()); // { value: 3 }
```

Notice that, we've stored `"INCREMENT"` and `"ADD"` as constants. That's because we were repeating them in our reducers, and there was a chance for typo. It's a good practice to store action types as constants in one place.

---

🎉 If you've made it this far, congratulations. With all the knowledge you have right now, you can start creating apps with Redux. Of course there's more left, but you've covered a significant part of the API. Well done!

---

4️⃣ `replaceReducer`: This method is used for replacing the current root reducer function with a new one. Calling this method will change the internal reducer function reference. This comes into play, when you're splitting your code for performance.

```js
const newRootReducer = combineReducers({
  existingSlice: existingSliceReducer,  
  newSlice: newSliceReducer
});

store.replaceReducer(newRootReducer);
```

<br>
<br>
## 👉 `bindActionCreators`

Now that we have some idea about action creators and dispatch, we can talk about this method.

```js
dispatch(increment())
dispatch(add(5))
```

This is how we've dispatched actions until now. But there's a simpler way to do this.

```js
const actions = bindActionCreators({ add, increment }, store.dispatch)

actions.increment()
actions.add(4)
```

`bindActionCreators` takes two arguments:
1. An object with all the action creators inside it.
2. The method we want to bind our action creators to.

It returns an object, which looks identical to the first argument we passed in. The only difference is, now we can call those methods directly, without calling dispatch explicitly.

What's the benefit of doing this?
> The only use case for bindActionCreators is when you want to pass some action creators down to a component that isn't aware of Redux, and you don't want to pass dispatch or the Redux store to it. - Redux Docs

Also, note that what we did is just plain JS, we could've achieved the same result by writing our own function that binds action creators to dispatch; without calling `bindActionCreators`.

<br>
<br>
## 👉 `combineReducers`

When you're developing a huge app where you can segregate data, it makes sense to have multiple reducers to reduce complexity. This method will combine all those multiple small reducers and return one reducer, generally called as root reducer, that our `createStore` method can use.

First, let's see why do we wanna have multiple reducers. Consider the following code.

```js
import { createStore } from "redux";

// constants
const CHANGE_USER_EMAIL = "CHANGE_USER_EMAIL";
const ADD_PRODUCT = "ADD_PRODUCT";

// action creators
const changeUserEmail = (email) => ({
  type: CHANGE_USER_EMAIL,
  payload: { email }
});
const addProduct = (product) => ({
  type: ADD_PRODUCT,
  payload: { product }
});

const initialState = {
  user: {
    name: "Mark",
    email: "mark@facebook.com"
  },
  cart: {
    products: []
  }
};

const reducer = (state = initialState, action) => {
  if (action.type === CHANGE_USER_EMAIL) {
    return {
      ...state,
      user: {
        ...state.user,
        email: action.payload.email
      }
    };
  }
  if (action.type === ADD_PRODUCT) {
    return {
      ...state,
      cart: {
        ...state.cart,
        products: [...state.cart.products, action.payload.product]
      } 
    };
  }
  return state;
};

const store = createStore(reducer);

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@facebook.com' }, cart: { products: [] } }

store.dispatch(changeUserEmail("mark@instagram.com"));

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@instagram.com' }, cart: { products: [] } }
```

As we can see this reducer is already looking a bit complex. As our app grows, data will be nested to deeper levels and size of the reducer will grow as well.

If we think about it, `user` and `cart` are two entirely different data points. We can split them into two different reducers. Let's do it.

```js
const initialState = {
  user: {
    name: "Mark",
    email: "mark@facebook.com"
  },
  cart: {
    products: []
  }
};

const userReducer = (user = initialState.user, action) => {
  if (action.type === CHANGE_USER_EMAIL) {
    return {
      ...user,
      email: action.payload.email
    };
  }
  return user;
}

const cartReducer = (cart = initialState.cart, action) => {
  if (action.type === ADD_PRODUCT) {
    return {
      ...cart,
      products: [...cart.products, action.payload.product]
    };
  }
  return cart;
}
```

Now we have two simple reducers and even the code looks clean. But `createStore` only takes a single reducer, which one should we pass?

Both. Using `combineReducers`.

```js
const rootReducer = combineReducers({
  user: userReducer,
  cart: cartReducer
});

const store = createStore(rootReducer);
```
 
This method takes an object, where keys can be anything but values should be our reducers. It will return a single reducer that can be passed to `createStore`.

Our complete code looks like this now.

```js
import { combineReducers, createStore } from "redux";

// constants
const CHANGE_USER_EMAIL = "CHANGE_USER_EMAIL";
const ADD_PRODUCT = "ADD_PRODUCT";

// action creators
const changeUserEmail = (email) => ({
  type: CHANGE_USER_EMAIL,
  payload: { email }
});
const addProduct = (product) => ({
  type: ADD_PRODUCT,
  payload: { product }
});

const initialState = {
  user: {
    name: "Mark",
    email: "mark@facebook.com"
  },
  cart: {
    products: []
  }
};

const userReducer = (user = initialState.user, action) => {
  if (action.type === CHANGE_USER_EMAIL) {
    return {
      ...user,
      email: action.payload.email
    };
  }
  return user;
};

const cartReducer = (cart = initialState.cart, action) => {
  if (action.type === ADD_PRODUCT) {
    return {
      ...cart,
      products: [...cart.products, action.payload.product]
    };
  }
  return cart;
};

const rootReducer = combineReducers({
  user: userReducer,
  cart: cartReducer
});

const store = createStore(rootReducer);

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@facebook.com' }, cart: { products: [] } }

store.dispatch(changeUserEmail("mark@instagram.com"));

console.log(store.getState()); 
// { user: { name: 'Mark', email: 'mark@instagram.com' }, cart: { products: [] } }
```

<br>
### 🌸 Store Enhancers

If you remember, `createStore` takes an optional argument - `enhancers`. 
Enhancers are nothing but higher order functions. They add some extra functionality to our store. For example, Redux dev tools is an enhancer. 

We won't talk much about enhancers here, because we will rarely create any new enhancers. Let's discuss this in detail in a separate article.

<br>
### ⛓ Middlewares

Middlewares provide us with the ability to intercept actions and do something we want to before that action reaches the reducers. We can log actions, log store state, log crash reports, etc.

Let's create a middleware for logging actions when they get dispatched.

```js
const logger = (store) => (next) => (action) => {
  console.log("DISPATCHED ACTION: ", action);
  next(action);
}
```

This is one of the simplest middlewares you can create. It logs the actions and then forwards the call to the rest of the middlewares and reducers that are in the pipeline.

But how do we use our new created middleware?

<br>
<br>
## 👉 `applyMiddleware`

This method will take a bunch of middlewares and return an enhancer. And enhancers go into the `createStore` function call.

```js
import { applyMiddleware, createStore } from 'redux'

const logger = (store) => (next) => (action) => {
  console.log("DISPATCHED ACTION: ", action);
  next(action);
}

const store = createStore(rootReducer, applyMiddleware(logger));
```

Now, every time we dispatch an action we'll have a log in our console.



🎉 With this we've covered out final method from Redux. That's all there is in the Redux API.

✨ It wouldn't be fair to not let you guys know that the modern Redux is different from what's discussed in this article. The basic concepts still apply but you'll have to write lesser code. Thanks to the Redux team. 🙏 Want to learn about it? [Click here](https://redux-toolkit.js.org/).

---

💡 I understand you won't get 100% of the things right now but it's good to be aware of all the tools you have under your belt.

💾 This article can act as guide for you when you want to revise something just before an interview or when you're implementing something. Save it!

🤝 If you'd like to read more of my upcoming articles, you can connect with me on [LinkedIn](https://www.linkedin.com/in/thesanjeevsharma) or [Twitter](https://www.twitter.com/thesnjvsharma).

🙏 Thank you for reading!