State management in react using redux-toolkit

Need of a state management library

What's the need for a state management library? can't we go with the useState hook provided by React to manage the states of our component?

Answer: To avoid the prop drilling we must use a state management library.

Redux

Redux is a state container for JavaScript apps, using redux we can manage the state of our app.

The whole redux logic requires four things:

  1. Store : A "store" is a js object that holds the application's global state
  2. Dispatch : dispatch is a method we need to call inorder to update our state
  3. Action : action is an object that describes "something that happened in the application"
  4. Reducers: reducers are functions where we write how our state should be updated based on the action provided.

working

Whenever an action is dispatched, the store will call the reducer function and based on the action provided the state will be updated.

Redux-toolkit

redux-toolkit is the official recommended approach for writing redux logic. redux-toolkit simplifies setting up the redux logic in react.

What issues does redux-toolkit solve?

  • Configuring a Redux store is too complicated
  • Redux requires too much boilerplate code

Installation

npm install @reduxjs/toolkit react-redux

Step1 : Create Store

// app/store.js
import  { configureStore }  from  '@reduxjs/toolkit'  
  
export  const store =  configureStore({  
	reducer:  {},  
})

Step2: Provide the store to React

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'
 
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Step3: Create a redux state slice

//counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
 
const initialState = {
  value: 0,
}
 
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})
 
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
 
export default counterSlice.reducer

Step4: Add Slice reducer to the store

// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
 
export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

Step5: Use redux state in the components

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
 
export function Counter() {
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
 
  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

createAsyncThunk for the api calls

A function that accepts a Redux action type string and a callback function that should return a promise.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
 
// First, create the thunk
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId: number, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  },
)
 
interface UsersState {
  entities: User[]
  loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
 
const initialState = {
  entities: [],
  loading: 'idle',
} satisfies UserState as UsersState
 
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
      ...
  },
  extraReducers: (builder) => {
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      state.entities.push(action.payload)
    })
  },
})
 
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))