Redux Toolkit In Depth

Redux Toolkit In Depth

ยท

9 min read

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

Now, what is redux? Redux is mainly used for state management.

We already understand that we should know about the basic principles of Redux before jumping to the Redux Toolkit.

Let's start. We have to know about the redux workflow. Redux has three parts.

i) Actions: Actions is the javascript object that contains some information from the app to the redux store. It has a type property that determined something that happened in the app.

ii) Reducers: Reducers determine how the state is changed according to the type of the actions.

iii) Store: Store saves the application's state. Actions can update store using dispatch keyword.

redux.drawio (1).png

If you get confused between actions and reducers, then I will try to explain this again.

Actions basically describe what to do through type property and Reducers determines how the task is done according to the type property. Single store just save the updated state.

Now jump to the code. You will be more clear about these.

const redux = require('redux');


//actions
const TheoryClass = "TheoryClass";

function theoryClass() {
    return {
        type: TheoryClass,
    }

}

//reducers (state, action)

const initialState = {
    countOfTheoryClass: 0,
}

const reducers = (state = initialState, action) => {
    switch (action.type) {
        case TheoryClass:
            return {
                ...state,
                countOfTheoryClass: state.countOfTheoryClass + 1,
            }
        default:
            return state;
    }
}


//store
//import createStore
const createStore = redux.createStore;

const store = createStore(reducers);
console.log('initial state', store.getState());

//subscribe- how the sore is updated

const unsubscribe = store.subscribe(() =>
    console.log('Present State ', store.getState())
)

//access the store

store.dispatch(theoryClass());
store.dispatch(theoryClass());

//call unsubscribe to stop the store from updating
unsubscribe();

I created a node project, install the redux and initialize the redux.

Now, Most of you have two questions in your mind. How can I use redux without React? Why am I not using redux with react.js in this blog?

Redux is not only used with React.js. We can use this with any other programming languages and frameworks.

Now, we try to understand the redux core concept. This is the reason, I am not using redux with react.js in the above code. But I will describe later how to use Redux Toolkit with React.js and how Redux logic is working under the hood. So, nothing to bother about the syntax of the above code. Focus on understanding the core concept for now.

Let's explain the above code.

Actions is returning a type TheoryClass. Reducers determine how the initial state is manipulated for the action's type.

We declare an initial state. There is a property countOfTheoryClass that value is 0. Reducers always received two parameters. Those are state and action.

We can access the action's type using action.type and write the logic for any type of action in the switch case. In TheoryClass case, copy the current state and update the state of countOfTheoryClass. First copy the state using ...state, because we shouldn't manipulate all properties of the initial state.

For example, if the initial state is like the bellow

const initialState = {
    countOfTheoryClass: 0,
    countOfBreak:0
}

Now, we don't want to change the countOfBreak, so the right approach is to copy the state and update only the property that you need.

Using this line const store = createStore(reducers);, reducers connceted to the store.

We can access the state using store.getState().

store.dispatch(theoryClass()); revokes the actions.

If we run the above code, the output will be like this.

Screen Shot 2022-06-17 at 2.05.28 PM.png

More Reducers: How to handle if you need more reducers? Let's think, you need how many practical classes happened and how many were canceled.

const redux = require('redux');
const combineReducers = redux.combineReducers;


//actions
const TheoryClass = "TheoryClass";
const TheoryClassCancel = "TheoryClassCancel";

function theoryClass() {
    return {
        type: TheoryClass,
    }

}

function theoryClassCancel() {
    return {
        type: TheoryClassCancel,
    }

}

const PracticalClass = "PracticalClass";
const PracticalClassCancel = "PracticalClassCancel";

function practicalClass() {
    return {
        type: PracticalClass,
    }
}

function practicalClassCancel() {
    return {
        type: PracticalClassCancel,
    }

}

//reducers (state, action)

const initialState = {
    countOfTheoryClass: 0,
}

const initialPracticalState = {
    countOfPracticalClass: 0,
}

const reducers = (state = initialState, action) => {
    switch (action.type) {
        case TheoryClass:
            return {
                ...state,
                countOfTheoryClass: state.countOfTheoryClass + 1,
            }
        case TheoryClassCancel:
            return {
                ...state,
                countOfTheoryClass: state.countOfTheoryClass - 1,
            }
        default:
            return state;
    }
}


const reducersPractical = (state = initialPracticalState, action) => {
    switch (action.type) {
        case PracticalClass:
            return {
                ...state,
                countOfPracticalClass: state.countOfPracticalClass + 1,
            }
        case PracticalClassCancel:
            return {
                ...state,
                countOfPracticalClass: state.countOfPracticalClass - 1,
            }
        default:
            return state;
    }
}


//store

const createStore = redux.createStore;

const rootReducer = combineReducers({
    theoryClassReducer: reducers,
    practicalClassReducer: reducersPractical,
})
const store = createStore(rootReducer);
console.log('initial state', store.getState());

//subscribe- how the sore is updated

const unsubscribe = store.subscribe(() =>
    console.log('Present State ', store.getState())
)

//access the store

// store.dispatch(theoryClass());
// store.dispatch(theoryClass());
// store.dispatch(theoryClassCancel());

//bindActionCreators (actionCreator, dispatch)

const bindActionCreators = redux.bindActionCreators;

const action = bindActionCreators({ theoryClass, theoryClassCancel }, store.dispatch);
const practicalAction = bindActionCreators({ practicalClass, practicalClassCancel }, store.dispatch);

action.theoryClass();
action.theoryClass();
action.theoryClassCancel();

practicalAction.practicalClass();
practicalAction.practicalClass();
practicalAction.practicalClassCancel();

//call unsubscribe to stop the store from updating
unsubscribe();

you see two new keywords, combineReducers and bindActionCreators. bindActionCreators helps to manage and dispatch all actions together.

For example: If we want to dispatch store with actions, we write this,

store.dispatch(theoryClass());
store.dispatch(theoryClass());
store.dispatch(theoryClassCancel());

using bindActionCreators we write the same thing like this

const action = bindActionCreators({ theoryClass, theoryClassCancel }, store.dispatch);
action.theoryClass();
action.theoryClass();
action.theoryClassCancel();

Let's Jump to the Redux Tookit

We will convert the above code with redux tookit and react.js.

There are some package buildin installed in the Redux Toolkit aka RTK.

i) Redux Thunk ii) immer and iii) redux devtools

We have introduced usedispatch, useSelector and slice.

usedispatch: usedispatch is a hook that invoked actions. It works like dispatch in the redux.

Now, create a react app and install redux toolkit, react-redux and redux.

Create a folder named features in src folder. Create a folder theory in the feature. You can create your file structure like this.

Screen Shot 2022-06-27 at 11.39.26 AM.png

theroySlice.js

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
    countOfTheoryClass: 0,
}

const theorySlice = createSlice({
    name: "theory",
    initialState,
    reducers: {
        theoryClass: (state, actions) => {
            state.countOfTheoryClass += actions.payload;
        },
        theoryClassCancel: (state, actions) => {
            state.countOfTheoryClass -= actions.payload;
        }
    }
});

export const { theoryClass, theoryClassCancel } = theorySlice.actions

export default theorySlice.reducer

Here we declare an initial state where countOfTheoryClass value is 0.

const initialState = {
    countOfTheoryClass: 0,
}

Now, give a name to the slice, and declare the initial state and reducers.

Then, go to the app folder and create store file in the folder.

store.js

import { configureStore } from "@reduxjs/toolkit";
import theorySlice from "../features/theory/theorySlice";
import practicalSlice from "../features/practical/practicalSlice";


const store = configureStore({
    reducer: {
        theory: theorySlice,


    }
});


export default store;

Here, just call the call reducers and export the store.

Don't forget to add provider and store to the index.js file.

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './app/store'
import { Provider } from 'react-redux'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Rapped the app file with Provider.

Now, time to call the data from the state and show it on the client-side.

theory.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { theoryClass, theoryClassCancel } from './theorySlice';

const Theory = () => {
    const theory = useSelector(state => state.theory.countOfTheoryClass);
    const dispatch = useDispatch();
    return (
        <div>
            <p>Total Theory class: {theory}</p>
            <button onClick={() => dispatch(theoryClass(1))}>+</button>
            <button onClick={() => dispatch(theoryClassCancel(1))}>-</button>
        </div>
    );
};

export default Theory;

useSelector call the present state and useDispatch call the actions. In this case, actions are in the theorySlice.js file.

The same steps are followed for practicalSlice.js.

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
    countOfPracticalClass: 0,
}

const practicalSlice = createSlice({
    name: "practical",
    initialState,
    reducers: {
        practicalClass: (state, actions) => {
            state.countOfPracticalClass += actions.payload;
        },
        practicalClassCancel: (state, actions) => {
            state.countOfPracticalClass -= actions.payload;
        }

    }
});

export const { practicalClass, practicalClassCancel } = practicalSlice.actions

export default practicalSlice.reducer

parctical.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { practicalClass, practicalClassCancel } from './practicalSlice';

export const Practical = () => {
    const dispatch = useDispatch();
    const practical = useSelector(state => state.practical.countOfPracticalClass);
    return (
        <div>
            <p>Total Practical Class:{practical}</p>
            <button onClick={() => dispatch(practicalClass(1))}>+</button>
            <button onClick={() => dispatch(practicalClassCancel(1))}>-</button>
        </div>
    );
};

Screen Shot 2022-06-27 at 12.26.49 PM.png

Install the redux dev tools extensions and explore the change of actions and state.

Screen Shot 2022-06-27 at 12.45.59 PM.png

Now, start to think about complex cases. One action is dependent on another action. In our example, If the same teacher takes both the theory and practical classes. When he is absent then both classes will be canceled. In this case, if the theory is canceled then the practical class will be canceled.

To solve this complex case, we will use extraReducers.

pactricalSlice.js

import { createSlice } from '@reduxjs/toolkit'
import { theoryClassCancel } from '../theory/theorySlice';

const initialState = {
    countOfPracticalClass: 10,
}

const practicalSlice = createSlice({
    name: "practical",
    initialState,
    reducers: {
        practicalClass: (state, actions) => {
            state.countOfPracticalClass += actions.payload;
        },
        practicalClassCancel: (state, actions) => {
            state.countOfPracticalClass -= actions.payload;
        }

    },
    extraReducers: builder => {
        builder.addCase(theoryClassCancel, (state) => {
            state.countOfPracticalClass -= 1;
        }
        );
    }

});

export const { practicalClass, practicalClassCancel } = practicalSlice.actions

export default practicalSlice.reducer

Now, Check the actions and state value using redux dev tools and explore the changes in value.

How to call APIs using redux toolkit. Let's explore.

Let's assume this is the API of students. https://jsonplaceholder.typicode.com/users

userSlice.js

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

const initialState = {
    users: [],
    loading: false,
    error: null,
}

export const fetchUser = createAsyncThunk('user/fetchUser', () => {
    return axios
        .get('https://jsonplaceholder.typicode.com/users')
        .then(response => response.data)
})

const userSlice = createSlice({
    name: 'user',
    initialState,
    extraReducers: builder => {
        builder.addCase(fetchUser.pending, state => {
            state.loading = true
        }
        )
        builder.addCase(fetchUser.fulfilled, (state, action) => {
            state.users = action.payload
            state.loading = false
            state.error = null
        }
        )
        builder.addCase(fetchUser.rejected, (state, action) => {
            state.error = action.error.message
            state.loading = false
            state.users = []
        }
        )
    }
});


export default userSlice.reducer

createAsyncThunk is the build-in function of redux toolkit. It accepts two parameters, actions, and a call-back function. The rest of the terms I explained before.

user.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUser } from './userSlice';

const User = () => {
    const users = useSelector(state => state.users.users);
    const dispatch = useDispatch();
    useEffect(() => {
        dispatch(fetchUser());
    })
    return (
        <div>
            {
                users.map((user, index) => {
                    return <p key={index}>{user.name}</p>
                })
            }
        </div>
    );
};

export default User;

Don't forget to add userSlice in the store.

import { configureStore } from "@reduxjs/toolkit";
import theorySlice from "../features/theory/theorySlice";
import practicalSlice from "../features/practical/practicalSlice";
import usersSlice from "../features/users/userSlice";

const store = configureStore({
    reducer: {
        theory: theorySlice,
        practical: practicalSlice,
        users: usersSlice,

    }
});


export default store;

That's it for now. Hopefully, you get the full picture of the Redux toolkit from this blog.

Happy Coding ๐Ÿ‘จโ€๐Ÿ’ป

ย