Write clean code in redux.

February 5, 2023

Redux is a very popular package to handle global state in the application. Redux allows you to manage state in a single place and keep changes in your app more predictable and traceable, making it easier to understand the changes happening in your app. In a small and simple app you do not even need redux you can use context API. If your application need state in many places, the app state is updated frequently, the app has a medium or large-sized codebase, and might be worked on by many people,then you can use redux. The beauty of redux is that we can use redux in react, react native and angular.

In the past writting redux code was a too much work because of the too much boilerplate code. Thanks to redux tool kit (RTK) writting redux code nowadays is way more easier than redux core. So let's start by comparing thecode we write in redux core vs redux tool kit.

Setup redux without RTK. (Not to do this way)

You do not need to remember this redux core example fully but when we see these in older codebase you need to know whats going on. If you want to explore redux core more or you are working in a older codebase with redux core you can always check this docs from redux.

//  add the dependencies
yarn add redux react-redux redux-thunk redux-devtools-extension
// creating the reducers
export default function cartReducer(state = initialState, action) {
  switch (action.type) {
    case 'cart/addItem':
      return {
        ...state,
        cart: action.payload,
      };
 
    case 'cart/clearCart':
      return { ...initialState };
 
    default:
      return state;
  }
}
 
export function addItem() {
  return {
    type: 'cart/addItem',
    payload: {[]},
  };
}
// create store
const rootReducer = combineReducers({
  cart: cartReducer,
});
 
const store = createStore(
  rootReducer,
  composeWithDevTools(
    // prettier-ignore
    applyMiddleware(thunk),
  ),
);

Problem that many developers used to face using redux core:

So to solve the problem for the developers redux toolkit was invented. RTK wraps around the core redux package, and contains API methods and common dependencies that we think are essential for building a redux app 😲.

Let's handle the application state in go to way. To start now you can either clone or extract templete provided from redux by clicking here or add in existing project.

Installing the dependencies

yarn add react-redux @reduxjs/toolkit

Create Slice

createSlice is a function where we write the state logic for a single state or a feature from bunch of reducers.

// cartSlice.js
import { createSlice } from '@reduxjs/toolkit';
 
const initialState = {
  cart: [],
};
 
const cartSlice = createSlice({
  name: 'cart',
  initialState,
  reducers: {
    addItem(state, action) {
      state.cart.push(action.payload);
    },
    deleteItem(state, action) {
      state.cart.filter((cartItem) => cartItem.id !== action.payload);
    },
    clearCart(state) {
      state.cart = [];
    },
  },
});
 
export const { addItem, deleteItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;

createSlice function accepts a configuration object with these following options:

In redux we used to handle the reducer with a specific action type with a switch case manually. RTK will generate the action name and type automatically from the name and method name of the reducer for example:

Configure Store

The standard method for creating a Redux store. It uses the low-level Redux core createStore method internally, but wraps that to provide good middleware and redux devtools out of the box when we use configureStore method.

// store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './features/cart/cartSlice';
 
const store = configureStore({
  reducer: {
    cart: cartReducer,
  },
});
 
export default store;

It basically helps developers experience with doing these tasks automatically:

Provider

import App from './App.jsx';
import { Provider } from 'react-redux';
import store from './store.js';
 
<Provider store={store}>
  <App />
</Provider>;

Provider the store to the children where we need the store to retrive the data and set the data from there. Redux also does the perfomance optimization automatically. You can see the difference of using context and redux by using the Profiler tab from the react devtools not redux devtools.

Get data from store

useSelector hook allows you to extract data from the redux store state for use in this component, using a selector function.

//cart.jsx
function Cart() {
  const cart = useSelector(getCart);
  // render component with data.
}
 
export default Cart;
 
// cartSlice.js
// ... existing cartSlice code
 
export const getCart = (state) => state.cart.cart;

useSelector hook has the state of entire store so we can isolate the getCart in the cartSlice file so that if we need the cart state in another component we can directly use that method without repeating useSelector((state) => state.cart.cart) in every where we use cart state.

Create action in the store

useDispatch hook returns a reference to the dispatch function from the Redux store. You may use it to dispatch actions as needed, the action that we exported from the slice.

// cart.jsx
import { useDispatch } from 'react-redux';
import { clearCart } from './cartSlice';
 
function Cart() {
  const dispatch = useDispatch();
  <div>
    ...
    <button onClick={() => dispatch(clearCart())}>Empty cart</button>;
  </div>;
}

API calls

Stop using redux if you are using redux just for data fetching. There are packages like tanstack query, RTK query for controlled data fetching and caching. However if you are using next js version 14 or later you can just use fetch api that will automatically cache the data and revalidate with some options.

// example of data fetching, caching and revalidating in next js.
const res = await fetch('https://...', { next: { tags: ['collection'] } });

Coming back to redux to perform API calls createAsyncThunk function is used. this function accepts a redux action type string and a callback function that should always return a promise. It generates promise lifecycle action types based on the action type prefix that you pass in, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions (pending, fulfilled, rejected) based on the returned promise.

// userSlice.js
 
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { userAPI } from './userAPI';
 
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId: number, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  },
)

We need to call the createAsyncThunk function with the action type name 'users/fetchByIdStatus' and callback function. The callback function is mandatory to be asynchronous because we need those three promise states to hanlde the status, error or data according to the promise result. Because the promise can success or fail.

// userSlice.js
const initialState = {
  data: [],
  status: 'idle',
  error: null,
};
 
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {},
  extraReducers: (builder) =>
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.data.push(action.payload);
        state.status = 'success';
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.status = 'error';
        state.error = action.error.message;
      }),
});

We use extraReducers and reducers to respond to an action in the slice . It may seem they are similar but the major difference is extraReducers provides the builder API where we can add case for different type of action kind of like a switch statement. However in reducer we just pass the name of the action function and handle the case when that reducer function is dispatched. for example if the promise is pending the it sets the status to "loading" and changed accordingly to the case type.

References

If you liked it share it with your friends or colleague