Understanding working of Redux, Redux Toolkit and React-Redux with hooks

·

9 min read

Redux

Redux is a library that can be used to manage state in large and complicated applications.

It allows to predictably update your app state.

We need to understand 4 major concepts in Redux to work with it easily.

State

It is not same as React state. State refers to a variable that may be updated later on, changes to state may cause changes to the UI.

In React, state can be an array, a string, a boolean but in Redux state will always be an object.

The name of variable does not matter much, it's important that we declare state in Redux as an object.

const state = {
    value : 0
}

0 here is telling, initially value is equal to 0.

We already know state in Redux is different than state in React. Why is it so? First state in Redux is an object.

The second reason we can use hook to change the state in React but in Redux we need a reducer function to update our state.

Reducer

Reducer is a function in Redux that takes two parameters. The current state and an action. Depending on the action, reducer may return a new state(update the state) or return the current state as it is.

const initialState = {
    value: 0,
}

const counterReducer = (state = initialState, action = {}) => {
    return state; 
}

The reducer will return the state as it is in this example. As you may see, the counterReducer is taking two parameters in the callback function. One is state initially equal to initialState, other is action which is initially equal to empty object.

'action' word is handled by redux toolkit. Do not worry about it. action is an object which is used to make change in the state. We can define the action.type as per our own convenience

const counterReducer = (state = initialState, action = {}) => {
    if(action.type === "counter/increment"){
        return {
            value: state.value + 1
        };
    }

    return state;
}

Just like in React, do not mutate the state.

This is how we will use the counterReducer now.

const newState = counterReducer(state, {
    type: "counter/increment"
});

Things we need to note here

1 . action is an object and it has a key called 'type'. 2 . You will never call the reducer function by yourself. Above is just an example. 3 . Instead of if else block, switch statements can also be used.

In redux, 'reducer' is a function that will receive the current state and an action. Based on the action which is the second parameter, reducer will return some state.

Store

Store is an object which contains the reducer and the state. When we want to make an update we will ask the store to make an update.

The store will use the reducer method to compute the new update.

Assuming the counterReducer function in above sections this is how we create a store in Redux. This is how we will create a store in redux

import { createStore } from "redux";

const store = createStore(counterReducer);

Till now we have seen that actions, state and store all are objects in Redux. reducer is a function which takes two arguments, state and action. We will never call reducer directly, we use store for it.

Dispatching actions

Till now we have seen the 3 important concepts of Redux. The fourth concept in redux is Dispatching actions.

We are not allowed to manually make changes to the state stored in the store. We always and remember this, always ask the store to make the change for us.

We do that by dispatching the action. WE KNOW THAT state, action and store are objects in redux.

We have seen the action example previously. To use an action, we have to use the

store.dispatch({action})
// example store.dispatch({type: "counter/increment})

When we call the store.dispatch() method we will call the reducer function to call a new state.

We can dispatch the action based on any event, one common use is dispatching the action when button is clicked.

const addButton = document.querySelector("#add-button");

addButton.addEventListener("click", () => {
    store.dispatch({ type: "counter/increment" });
});

Redux has 4 pillars. state, action, reducer and store. We declare store, action and store as objects and we dispatch the action using reducer which updates and returns the state.

Redux Toolkit

In react we use redux toolkit. It was created as to make common redux tasks more efficient to write.

'immer' is a javascript library which allows to immutably update the state while still writing the same JS code.

Slices

As we make more number of action, store becomes more complex. So we use the 'slice' in redux toolkit.

A redux slice is a collection of a reducer logic and actions for a single feature of our app.

This is how we can create a slice in Redux toolkit

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

const counterSlice = createSlice({
    name: "counter",
    initialState: {
        value : 0,
    },
    reducers:{
        increment: (state) => {
            state.value+=1;
        }
    },
});

We are not prefixing the reducers with counter/ anymore as slice is only looking for actions related to the counter that we are providing in the name. We have created a reducer function called increment.

In this 'immer' is being loaded by the redux toolkit, so we can write the increment logic as if we are mutating the state(which we are not doing, neither we should mutate the state directly).

** We don't have to import the immer. It is already loaded by redux toolkit @reduxjs/toolkit **

Configuring the store

To configure the store as per our requirement we need to import 'configureStore' like this

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

const store = configureStore({
    reducer: counterSlice.reducer,
});

As we notice that configureStore like createSlice is taking an object as an argument.

Putting everything together for redux toolkit, this is how we will do it.

import {configureStore, createSlice} from '@reduxjs/toolkit' 

const counterSlice = createSlice({
    name:"counter",
    initialState: {
        value : 0
    },
    reducers: {
        increment: (state) => {
            state.value +=1;
        },
    }
});

const store = configureStore({
    reducer : counterSlice.reducer
})

Notice that we are creating the counterSlice.reducer and not counterSlice.reducers. counterSlice.reducer is created and handled by Redux toolkit.

Actions and payload

In redux this is how we dispatched an action

store.dispatch({
    type: "counter/increment"
})

In redux toolkit we have a function that helps us to exact reducer from our store.

This is how we extract 'increment' function using counterSlice.actions() method

const {increment} = counterSlice.actions

'increment' name is same as we gave in reducers in the counterSlice that we created in previous sections. As we see this is object destructuring, we can access the increment function this way as well

const incrementHandler = counterSlice.actions.increment

We can de structure our functions more if we have more reducers

const {increment, decrement, reset} = counterSlice.actions

Supplying payloads

Till now we have discussed reducers which work with a fixed step of increment, decrement. In real world we need to have different requirements, we can have increment by 5 or 10 or 100 as per need. In such case we make an action that increments by a number(this is just an example for understanding), the value(number) is called a payload.

When we were discussing about the reducers, we talked that reducer will take 2 arguments. One is state and other is action

const counterReducer = (state = initialState, action = {}) => {
    if(action.type === "counter/increment"){
        return {
            value: state.value + 1
        };
    }

    return state;
}

To access the value that was passed to the action while it was called, we used action.payload

This is how we will write code if we want to use payload


import {configureStore, createSlice} from '@reduxjs/toolkit';

const counterSlice = createSlice({
    name: "counter",
    initialState: {
        value : 0,
    },
    reducers : {
        incrementBy : (state, action) => {
            state.value += action.payload;
        },
        increment : state => {
            state.value += 1;
        }
    }
});

const store = configureStore({
    reducer: counterSlice.reducer
});

const {incrementBy, increment} = counterSlice.actions

console.log(store.dispatch(incrementBy(5)));

console.log(increment());

console.log(store.dispatch(increment()))

We see that we are using the payload only by passing 'incrementBy()' function as argument to store.dispatch

We might have de structured our reducers but we will not get any result if we use them directly.

increment() will not give us any result. We will have to use store.dispatch(increment()) for increment() logic to act.

React Redux

We have been knowing about Redux and Redux toolkit, it makes sense to know how to use Redux now in our apps

npx create-react-app my-app --template redux

This command will create a new React app with redux template. In react app, we will use the @reduxjs/toolkit.

React-redux will provide us specific components and hooks that will allow us to use Redux in our application.

Provider

In React, we can make the store available to any child component by wrapping ,entire app with provider component. This allows to dispatch events from any component inside the app as long as it is the child of

import { createRoot } from 'react-dom/client';
import { store } from './store.js';
import { Provider } from 'react-redux';

const root = document.querySelector('#react-root');

createRoot(root).render(<Provider store={store}>
    <App />
</Provider>);

Here store will be created by us and imported in App.js and provider is imported from react-redux.

useSelector Hook

The useSelector hook allows us to extract the data from our redux store. When we use the 'useSelector' hook inside of a component, our component will agree to the change of the store and whenever the state is changed in the store, the component will be called again to update the state.

Following is an example of useSelector hook. We will see the explanation in later section.

import {useSelector} from "react-redux";

function Counter() {
    const counter = useSelector(state => state.value);

    return <h1>Counter: {counter}</h1>
}

useSelector is taking a callback function.

useDispatch hook

We created the store earlier and then used the .dispatch method to dispatch an reducer.

const {incrementBy, increment} = counterSlice.actions

console.log(store.dispatch(incrementBy(5)));

console.log(store.dispatch(increment()))

Now in react redux we can use the useDispatch() hook by creating an instance of useDispatch hook and then pass the reducer in the instance created

import {useDispatch} from 'react-redux';

const dispatchSample = useDispatch();
console.log(dispatchSample(increment());

Above code is just for example. A more practical example will be this


import {useDispatch} from "react-redux";
import {increment} from "./store.js";

export default function Counter() {
    const dispatch = useDispatch();

    return <button onClick={() => dispatch(increment())}>Add 1</button>;
}

As a rule of hooks, we must not call useDispatch() inside any condition or an event handler. useDispatch() should always be called on top of the component.