Complete redux toolkit (Part -3 )

WHAT TO KNOW - Sep 10 - - Dev Community

Redux Toolkit: The Power of Simplicity (Part 3: Advanced Techniques)

Introduction

This is the third part of our series exploring Redux Toolkit, a library designed to simplify Redux development. In the previous parts, we covered the fundamentals of Redux and how Redux Toolkit streamlines the process of creating reducers, actions, and managing state. Now, we delve deeper into advanced techniques, enhancing our ability to build robust and maintainable applications.

This article will focus on:

  • Sagas and Thunks: Understanding asynchronous actions and how to manage them effectively.
  • Selectors and Memoization: Optimizing performance by selectively extracting and caching data.
  • Custom Middleware: Extending Redux Toolkit's functionality with tailored solutions.
  • Testing Your Redux Logic: Ensuring code quality and stability through comprehensive testing.

Async Operations: Sagas and Thunks

Redux, by its nature, is designed for synchronous state updates. However, real-world applications often involve asynchronous operations, such as fetching data from an API or interacting with a server. To handle these scenarios, we utilize middleware.

1. Thunks: Simple Asynchronous Operations

Thunks provide a straightforward way to handle asynchronous actions. A thunk is simply a function that takes the dispatch function as an argument and returns another function. This inner function can perform asynchronous operations and dispatch actions to update the state.

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

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

const fetchData = createAsyncThunk('fetchData', async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    return response.json();
  } catch (error) {
    return error.message;
  }
});

const dataSlice = createSlice({
  name: 'data',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchData.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchData.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchData.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export const { reducer, actions } = dataSlice;

// Usage
dispatch(fetchData()); // initiates the async operation
Enter fullscreen mode Exit fullscreen mode

This example demonstrates how we can use createAsyncThunk to define an asynchronous action that fetches data. We handle the pending, fulfilled, and rejected states within the extraReducers of our slice.

2. Sagas: Complex Asynchronous Workflows

For more intricate asynchronous operations, such as managing multiple requests, handling side effects, or implementing complex logic, Redux Saga provides a powerful framework. Sagas use generators to define asynchronous processes, allowing for easy management of side effects and concurrency.

import { takeEvery, call, put } from 'redux-saga/effects';
import { fetchData } from '../features/dataSlice';

function* fetchDataSaga() {
  try {
    const data = yield call(fetch, 'https://api.example.com/data');
    yield put(fetchData.fulfilled(data.json()));
  } catch (error) {
    yield put(fetchData.rejected(error.message));
  }
}

function* rootSaga() {
  yield takeEvery(fetchData.type, fetchDataSaga);
}

export default rootSaga;
Enter fullscreen mode Exit fullscreen mode

Here, we use takeEvery to listen for the fetchData action. When triggered, the fetchDataSaga generator performs the API call and dispatches the corresponding fulfilled or rejected actions.

Selectors and Memoization

As our applications grow, the state tree becomes more complex, and accessing specific data points efficiently becomes crucial. Selectors offer a powerful way to extract and transform data from the state, enhancing code readability and performance.

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

const selectData = (state) => state.data.data;

const selectFilteredData = createSelector(
  selectData,
  (data) => data.filter((item) => item.status === 'active')
);
Enter fullscreen mode Exit fullscreen mode

We use createSelector to create a derived selector, selectFilteredData, that filters the data based on the 'status' property. This selector uses memoization, automatically caching the result based on its dependencies. When the dependencies change, the selector re-evaluates and returns a new result.

Custom Middleware

Redux Toolkit provides a flexible framework for creating custom middleware to extend its functionality. Middleware sits between the actions and the reducers, allowing you to intercept actions, perform logic, and modify the state before it reaches the reducers.

const logMiddleware = (store) => (next) => (action) => {
  console.log('Action dispatched:', action);
  const result = next(action);
  console.log('State after dispatch:', store.getState());
  return result;
};

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logMiddleware),
});
Enter fullscreen mode Exit fullscreen mode

In this example, we create a middleware logMiddleware that logs the dispatched actions and the state after each dispatch. We then register this middleware in our configureStore function.

Testing Your Redux Logic

Testing is essential for ensuring the correctness and robustness of your Redux logic. Redux Toolkit provides tools that make testing easier:

  • Testing Thunks with createAsyncThunk: Use createAsyncThunk to define your async actions and easily mock network requests within your tests.
  • Testing Reducers with createReducer: Test the state transformations in your reducers by providing different input actions and checking the resulting state.
  • Testing Sagas with redux-saga-test-plan: The redux-saga-test-plan library allows you to test your Sagas by controlling the flow of the generator function and validating the dispatched actions.

Example: Testing a thunk:

import { fetchData } from '../features/dataSlice';
import { configureStore } from '@reduxjs/toolkit';
import { fetch } from 'cross-fetch'; // or your preferred fetch library

jest.mock('cross-fetch'); // mock the fetch API

describe('fetchData thunk', () => {
  it('should dispatch the correct actions', async () => {
    const mockData = { data: 'test data' };
    fetch.mockReturnValue(Promise.resolve({
      json: () => Promise.resolve(mockData),
    }));

    const store = configureStore({
      reducer: rootReducer,
    });
    const dispatch = store.dispatch;
    const result = await dispatch(fetchData());

    expect(result.type).toEqual(fetchData.fulfilled.type);
    expect(fetch).toHaveBeenCalledWith('https://api.example.com/data');
    expect(store.getState().data.data).toEqual(mockData);
  });
});
Enter fullscreen mode Exit fullscreen mode

This test mocks the fetch API, dispatches the fetchData action, and verifies the dispatched actions and the updated state.

Conclusion

Redux Toolkit provides a powerful and convenient way to manage your Redux state, while offering advanced features for managing asynchronous operations, optimizing performance, and extending functionality. By leveraging the techniques presented in this article, you can build robust and maintainable applications, ensuring efficient data management and a smooth user experience.

Key Takeaways:

  • Thunks and Sagas: Choose the appropriate approach for handling asynchronous operations based on the complexity of your logic.
  • Selectors and Memoization: Optimize data access and performance with derived selectors and memoization.
  • Custom Middleware: Extend Redux Toolkit's capabilities with tailored solutions for specific needs.
  • Testing: Write comprehensive tests for your Redux logic to ensure code quality and stability.

This article explores the power of Redux Toolkit beyond its fundamental usage. By mastering these advanced techniques, you can unleash the full potential of this library and build sophisticated, well-structured Redux applications. Remember to experiment, explore, and always aim for clean and maintainable code.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player