Redux Tutorial

1. Introduction to Redux

1.1 What is Redux & why Redux?

  • A small JS Library
  • for managing medium/large amount of states globally in your app.
  • useContext + useReducer Hook ideas will help you to understand redux.

1.2 Some common terms related to redux

  • React-redux: redux is used with some common packages such as react-redux
  • redux-toolkit : recommended way to write redux logic for building redux app easily and avoiding mistakes.
  • redux devtools extension: helps to debug redux app easily.

1.3 how redux works?

  • define state.
  • dispatch an Action.
  • Reducer update state based on Action Type.
  • store will update the view

Screenshot 2022-05-17 at 19 37 57

2. redux core concept

  • State: consider what states you want to manage

    // define states
    count: 0;
    const initialState = { count: 0 };
    const initialState2 = { users: [{ name: "anisul islam" }] };
  • Action: actions are object that have 2 things- type & payload

    // define constants
    const INCREMENT = "INCREMENT";
    const DECREMENT = "DECREMENT";
    const ADD_USER = "ADD_USER";
    
    // dispatch(Action)
    {
      type: INCREMENT,
    }
    {
      type: DECREMENT,
    }
    {
      type: ADD_USER,
      payload: {
        name: "rafiqul islam",
      }
    }
  • Reducer: reducers are pure function which handles all logic. it updates the state depends on action type

    // crate reducer
    const counterReducer = (state = initialState, action) => {
      switch (action.type) {
        case INCREMENT:
          return {
            ...state,
            count: state.count + 1,
          };
        case DECREMENT:
          return {
            ...state,
            count: state.count - 1,
          };
    
        default:
          return state;
      }
    };
  • Store: It holds the states. It has 3 important methods- getState(), dispatch(), suscribe()

    // 4. store - getState(), dispatch(), subscribe()
    
    // create store
    const store = createStore(counterReducer);
    
    store.subscribe(() => {
      console.log(store.getState());
    });
    
    // dispatch action
    store.dispatch(incrementCounter());
    store.dispatch(incrementCounter());
    store.dispatch(incrementCounter());
    store.dispatch(decrementCounter());

3. Complete Counter App

  • example of counter app

    const { createStore } = require("redux");
    
    const INCREMENT = "INCREMENT";
    const DECREMENT = "DECREMENT";
    const RESET = "RESET";
    
    const initialCounterState = {
      count: 0,
    };
    
    const incrementCounter = () => {
      return {
        type: INCREMENT,
      };
    };
    const decrementCounter = () => {
      return {
        type: DECREMENT,
      };
    };
    const resetCounter = () => {
      return {
        type: RESET,
      };
    };
    
    const counterReducer = (state = initialCounterState, action) => {
      switch (action.type) {
        case INCREMENT:
          return {
            ...state,
            count: state.count + 1,
          };
        case DECREMENT:
          return {
            ...state,
            count: state.count - 1,
          };
        case RESET:
          return {
            ...state,
            count: 0,
          };
    
        default:
          state;
      }
    };
    
    const store = createStore(counterReducer);
    
    store.subscribe(() => {
      console.log(store.getState());
    });
    
    store.dispatch(incrementCounter());
    store.dispatch(incrementCounter());
    store.dispatch(decrementCounter());
    store.dispatch(resetCounter());

4. payload in action

  • example

    const { createStore } = require("redux");
    
    const GET_PRODUCTS = "GET_PRODUCTS";
    const ADD_PRODUCTS = "ADD_PRODUCTS";
    
    const initialProductState = {
      products: ["sugar", "salt"],
      numberOfProducts: 2,
    };
    
    const getProductAction = () => {
      return {
        type: GET_PRODUCTS,
      };
    };
    const addProductAction = (product) => {
      return {
        type: ADD_PRODUCTS,
        payload: product,
      };
    };
    
    const productsReducer = (state = initialProductState, action) => {
      switch (action.type) {
        case GET_PRODUCTS:
          return {
            ...state,
          };
        case ADD_PRODUCTS:
          return {
            products: [...state.products, action.payload],
            numberOfProducts: state.numberOfProducts + 1,
          };
    
        default:
          return state;
      }
    };
    
    const store = createStore(productsReducer);
    
    store.subscribe(() => {
      console.log(store.getState());
    });
    
    store.dispatch(getProductAction());
    store.dispatch(addProductAction("pen"));
    store.dispatch(addProductAction("pencil"));

5. Multiple reducers & combine multiple reducers

  • example

    const { createStore, combineReducers } = require("redux");
    
    // product constants
    const GET_PRODUCTS = "GET_PRODUCTS";
    const ADD_PRODUCTS = "ADD_PRODUCTS";
    
    // cart constants
    const GET_CART_ITEMS = "GET_CART_ITEMS";
    const ADD_CART_ITEMS = "ADD_CART_ITEMS";
    
    // product states
    const initialProductState = {
      products: ["sugar", "salt"],
      numberOfProducts: 2,
    };
    
    // cart states
    const initialCartState = {
      cart: ["sugar"],
      numberOfProducts: 1,
    };
    
    // product actions
    const getProductAction = () => {
      return {
        type: GET_PRODUCTS,
      };
    };
    const addProductAction = (product) => {
      return {
        type: ADD_PRODUCTS,
        payload: product,
      };
    };
    
    // cart actions
    const getCartAction = () => {
      return {
        type: GET_CART_ITEMS,
      };
    };
    const addCartAction = (product) => {
      return {
        type: ADD_CART_ITEMS,
        payload: product,
      };
    };
    
    const productsReducer = (state = initialProductState, action) => {
      switch (action.type) {
        case GET_PRODUCTS:
          return {
            ...state,
          };
        case ADD_PRODUCTS:
          return {
            products: [...state.products, action.payload],
            numberOfProducts: state.numberOfProducts + 1,
          };
    
        default:
          return state;
      }
    };
    
    const cartReducer = (state = initialCartState, action) => {
      switch (action.type) {
        case GET_CART_ITEMS:
          return {
            ...state,
          };
        case ADD_CART_ITEMS:
          return {
            cart: [...state.cart, action.payload],
            numberOfProducts: state.numberOfProducts + 1,
          };
    
        default:
          return state;
      }
    };
    
    const rootReduer = combineReducers({
      productR: productsReducer,
      cartR: cartReducer,
    });
    
    const store = createStore(rootReduer);
    
    store.subscribe(() => {
      console.log(store.getState());
    });
    
    store.dispatch(getProductAction());
    store.dispatch(addProductAction("pen"));
    store.dispatch(getCartAction());
    store.dispatch(addCartAction("salt"));

6. Middleware

  • for extra features, middlepoint of dispatching an action and handledby reducer, performing async tasks, login etc.

  • Example of popular redux middlewares packages: redux-logger, redux-thunk

  • npm install redux-logger

  • example

    const { createStore, combineReducers, applyMiddleware } = require("redux");
    const { default: logger } = require("redux-logger");
    
    // product constants
    const GET_PRODUCTS = "GET_PRODUCTS";
    const ADD_PRODUCTS = "ADD_PRODUCTS";
    
    // product states
    const initialProductState = {
      products: ["sugar", "salt"],
      numberOfProducts: 2,
    };
    
    // product actions
    const getProductAction = () => {
      return {
        type: GET_PRODUCTS,
      };
    };
    const addProductAction = (product) => {
      return {
        type: ADD_PRODUCTS,
        payload: product,
      };
    };
    
    const productsReducer = (state = initialProductState, action) => {
      switch (action.type) {
        case GET_PRODUCTS:
          return {
            ...state,
          };
        case ADD_PRODUCTS:
          return {
            products: [...state.products, action.payload],
            numberOfProducts: state.numberOfProducts + 1,
          };
    
        default:
          return state;
      }
    };
    
    const store = createStore(productsReducer, applyMiddleware(logger));
    
    store.subscribe(() => {
      console.log(store.getState());
    });
    
    store.dispatch(getProductAction());
    store.dispatch(addProductAction("pen"));

7. API Calling – async actions using redux-thunk

  • example

    // async actions - api calling
    // api url - https://jsonplaceholder.typicode.com/todos
    // middleware- redux-thunk
    // axios api
    
    const { default: axios } = require("axios");
    const { createStore, applyMiddleware } = require("redux");
    const reduxThunk = require("redux-thunk").default;
    
    // define constants
    const GET_TODOS_REQUEST = "GET_TODOS_REQUEST";
    const GET_TODOS_SUCCESS = "GET_TODOS_SUCCESS";
    const GET_TODOS_FAILED = "GET_TODOS_FAILED";
    const TODOS_URL = "https://jsonplaceholder.typicode.com/todos";
    
    // define state
    const initialTodosState = {
      todos: [],
      isLoading: false,
      error: null,
    };
    
    const getTodosRequest = () => {
      return {
        type: GET_TODOS_REQUEST,
      };
    };
    
    const getTodosSuccess = (todos) => {
      return {
        type: GET_TODOS_SUCCESS,
        payload: todos,
      };
    };
    const getTodosFailed = (error) => {
      return {
        type: GET_TODOS_FAILED,
        payload: error,
      };
    };
    
    const todosReducer = (state = initialTodosState, action) => {
      switch (action.type) {
        case GET_TODOS_REQUEST:
          return {
            ...state,
            isLoading: true,
          };
        case GET_TODOS_SUCCESS:
          return {
            ...state,
            todos: action.payload,
            isLoading: false,
          };
        case GET_TODOS_FAILED:
          return {
            ...state,
            isLoading: false,
            error: action.payload,
          };
    
        default:
          state;
      }
    };
    
    // async action creator
    // thunk-middleware allows us to return a function isntead of obejct
    const fetchData = () => {
      return (dispatch) => {
        dispatch(getTodosRequest());
        axios
          .get(TODOS_URL)
          .then((res) => {
            const todos = res.data;
            const titles = todos.map((todo) => todo.title);
            dispatch(getTodosSuccess(titles));
          })
          .catch((err) => {
            const error = err.message;
            dispatch(getTodosFailed(error));
          });
      };
    };
    
    const store = createStore(todosReducer, applyMiddleware(reduxThunk));
    
    store.subscribe(() => {
      console.log(store.getState());
    });
    
    store.dispatch(fetchData());

    8. React-redux counter example

    • install redux and react-redux package
    • we will make a counter app; first we will build with state and then we will do this with react-redux. example

         // App.js
         import React, { useState } from "react";
      
         // state - count:0
         // action - increment, decrement, reset
         /*
         reducer - handle logic for state update
          count => count + 1
          count => count - 1
          count => 0
         */
      
         const App = () => {
           const [count, setCount] = useState(0);
      
           const handleIncrement = () => {
             setCount((count) => count + 1);
           };
      
           const handleReset = () => {
             setCount(0);
           };
      
           const handleDecrement = () => {
             setCount((count) => count - 1);
           };
      
           return (
             <div>
               <h1>React Redux Example</h1>
               <h2>Count : {count}</h2>
               <button onClick={handleIncrement}>Increment</button>
               <button onClick={handleReset}>Reset</button>
               <button onClick={handleDecrement}>Decrement</button>
             </div>
           );
         };
      
         export default App;
         
  • now we will make the same counter app using react-redux

      // step 1: create constants
      //services/constants/counterConstants.js
        export const INCREMENT = "INCREMENT";
        export const RESET = "RESET";
        export const DECREMENT = "DECREMENT";
    
      // step 2: create actions 
      //services/actions/counterActions.js
      // action - increment, decrement, reset
    
        import { DECREMENT, INCREMENT, RESET } from "../constants/counterConstants";
    
        export const incrementCounter = () => {
          return {
            type: INCREMENT,
          };
        };
    
        export const resetCounter = () => {
          return {
            type: RESET,
          };
        };
    
        export const decrementCounter = () => {
          return {
            type: DECREMENT,
          };
        };
        
        
        // step 3: create reducers 
        //services/reducers/counterReducer.js
            /*
          reducer - handle logic for state update
           count => count + 1
           count => count - 1
           count => 0
          */
          import { DECREMENT, INCREMENT, RESET } from "../constants/counterConstants";
    
          const initialState = { count: 0 };
    
          const counterReducer = (state = initialState, action) => {
            switch (action.type) {
              case INCREMENT:
                return {
                  ...state,
                  count: state.count + 1,
                };
              case RESET:
                return {
                  ...state,
                  count: 0,
                };
              case DECREMENT:
                return {
                  ...state,
                  count: state.count - 1,
                };
    
              default:
                return state;
            }
          };
    
          export default counterReducer;
    
        // step 4: create store
        // npm install redux
        // src/store.js
            import { createStore } from "redux";
            import counterReducer from "./services/reducers/counterReducer";
            const store = createStore(counterReducer);
            export default store;
            
         // step 5: provide store in index.js
         // npm install react-redux
            import React from "react";
            import { createRoot } from "react-dom/client";
            import { Provider } from "react-redux";
    
            import App from "./App";
            import store from "./store";
    
            const container = document.getElementById("root");
            const root = createRoot(container);
    
            root.render(
              <Provider store={store}>
                <App />
              </Provider>
            );
    
          // step 6: use store in anywhere in your app. for example in Counter.js
          import React from "react";
          import { useDispatch, useSelector } from "react-redux";
          import {
            incrementCounter,
            decrementCounter,
            resetCounter,
          } from "./services/actions/counterAction";
    
          const Counter = () => {
            const count = useSelector((state) => state.count);
            const dispatch = useDispatch();
    
            const handleIncrement = () => {
              dispatch(incrementCounter());
            };
    
            const handleReset = () => {
              dispatch(resetCounter());
            };
    
            const handleDecrement = () => {
              dispatch(decrementCounter());
            };
    
            return (
              <div>
                <h1>React Redux Example</h1>
                <h2>Count : {count}</h2>
                <button onClick={handleIncrement}>Increment</button>
                <button onClick={handleReset}>Reset</button>
                <button onClick={handleDecrement}>Decrement</button>
              </div>
            );
          };
    
          export default Counter;
    
          // create actions & constants
          // create reducers
          // create & provide store
          // useSelector, useDispatch

GitHub

View Github