Redux Overview and Basic Setup with Sagas and TypeScript (Code Snippets)

Hanwen Zhang
8 min readNov 13, 2022

--

Redux is a library for state management, it controls in a single object — store; no matter how deep you are, no need for lifting the state up.

What is Redux?

  • redux creates a store, like a database, so sub-components can directly access values through the store instead of relying on parents(grandparents), whereas react uses a component tree that talks to each other through layers, the local state passed down to props
  • only one central data (state) store can be used inside our components
  • subscribe to the store for retrieving information, dispatch actions if you need to change anything

Why do we use Redux? Advantages?

  • Redux — A Predictable State Container for JS Apps
  • Avoid complicated communication for large-scale application
  • Avoid excessive lifting state up in ReactJS
  • Excellent tool for time-traveling debugging
  • Better state management

Three Principles of Redux

  1. Single source of truth — keep all data in the store
  2. The state is a read-only — immutable, persistent data structure, the only way to change the state is to emit an action
  3. Changes are made with pure function (reducer) — changes need actions

How Redux works

  • components NEVER directly manipulate the stored data, we have to set up a reducer for mutating
  • reducer functions take input and transform that input, reduce it like reducing a list of numbers to the sum, output a new result
  • components dispatch actions that trigger certain actions, then forward actions to reducers
  • when the state in that data store is updated, subscribing components are notified, so that they can update their UI

Redux Data Flow

  • Redux store is the place we save the state
  • getState is the method to get the state
  • action & reducer is the method to change the mapStateToProps

How to set up Redux? How do you create a store?

  • The store will be generated based on a reducer that analyzes behaviors and modifies the current local state
  • Redux store is like a global state, available to all children
  • Redux stroe is the values in your redux store, like the data from the database
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import reducer from "./reducer";
import App from "./App";

const store = createStore(reducer);

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

What does <Provider> do

  • <Provider store={store}> </Provider>
  • provider to inform the whole structure, for the provider layer, everything inside would be props.children
  • provider is like passing down everything to the children, we pass our store down to every level via createStore(reducer)

Subscribe to the store

  • import { connect } from "react-redux"
  • ConnectedApp = connect()(App)
const ConnectedApp = connect(	//here we use the connection function, connect will create the HOC wrapper that takes the APP
mapStateToProps, // on value - get the value from the store, make sure the component is hook up with the store (display)
mapDispatchToProps // on handler/actions - the action we need (user interaction)
)(App);

store

  • As the single source of truth, the store is the most important part of Redux because it is where your application state lives.
  • There is only one single store where all of the application’s state lives in. You can only change the state by dispatching an action to the store.

Action

  • define actions -> like an action generator
  • An object includes the action type and/or payload => the content you gonna use to make the change and dispatch action
  • What did you try to do? like number increase, decrease?
  • dispatch the action to the reducer
  • the action is going through to the reducer which analyzes the action

Reducer

  • an event (action) listener, is a function that is connected to the store.
  • whenever you dispatch an action to the store, All the reducers “listen” to see if this action type is what they are looking for.
  • Expecting all types of action as defined.
  • reply on the input and the local state at the moment
  • => analyze behavior and modify current local state
  • Pure function, A input -> A output a + b = c (same input with consistent output)
  • A pure function is static when we do not perform the render.
  • only has one reducer function: the “root reducer” function that you will pass to createStore later on
  • no side-effect, the output will be predictable (pure function)

How do you group different reducers?

  • combineReducers({ , , , })

Using ... vs .push() to manipulate the store

  • using .push() to push a new item to the state is no good in redux since push manipulates the existing array in the existing state.
  • instead, we can use the spread operator ... to get the copy of the array, and then return the manipulated one.
  • return {...state, todo: [...state.todo, action.payload], text: ""}

Using .concat() vs ... to manipulate the store

  • updatedItems = state.items.concat(action.item);
  • add the item which returns a new array (in an immutable way -> good)
  • do not use push which adds to the existing array

Redux Flow

  • ReactJS -> setState() -> local state update -> UI re-rendering -> Done
  • ReactRedux -> emit an action (dispatch an action) -> Reducer will calculate next state (analyze action) -> Component subscribing to the store data re-rendering

useSelector & useDispatch

  • useSelector — get state data
  • useDispactch — dispatch actions
const { useSelector, useDispatch } from "react-redux";
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter)
const incrementHandler = () => dispatch({type: "increment" });
const increaseHandler = () => dispatch({type: "increase", amount: 10 }); //with a payload

Middleware

What is middleware?

  • integrate all the different software systems and make them work together
  • it provides common services and capabilities to applications outside of what is offered by the operating system
  • code you can put between the framework receiving a request, and the framework generating a response.

Why do we need redux middleware?

  • it provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
  • people use Redux middleware for implementing async action calls
  • if we do not use the middleware, we can only do actions when the API server is not involved
  • middleware allows you to call the action creators that return a function(thunk) which takes the store dispatch method as the argument
  • which is afterward used to dispatch the synchronous action after the API or side effects have been finished.

What is Redux-saga

  • side effect management library (asynchronous things like data fetching and impure things like accessing browser cache).

How to create middleware in Redux?

  • Using Redux Thunk for Redux Middleware
  • apply something extra in the middle, like a middle layer
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux"; //import applyMiddleware
import { Provider } from "react-redux";
import thunk from "redux-thunk"; //import thunk
import rootReducer from "./store/reducer";
import App from "./App";

const store = createStore(
rootReducer,
applyMiddleware(thunk) //the middleware will expose to the whole flow, apply thunk middleware via applyMiddleware()
);

ReactDOM.render( //store goes through the whole project, including the middleware
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

How to apply middleware in Redux?

action.js

  • the action is going through to the reducer that analyzes the action
  • The thunk middleware allows us to write functions that get dispatch and getState as arguments.
  • dispatch() used to dispatch the synchronous action after the API or side effects have been finished.
  • getState() gets the current state of the whole store
let timer;
export const timerUpdate = () => (dispatch, getState) => {
clearInterval(timer);
timer = setInterval(() => {
dispatch(incAction());
}, 1000);
}
export const timerStopUpdate = () => (dispatch, getState) =>
clearInterval(timer);

//TO-DO LIST
const textAction = (item) => {
return {
type: "TEXT",
payload: item,
};
};
const addAction = () => (dispatch, getState) => {
const inputText = getState().tdListReducer.text; //getState() gets the current state in the whole store
dispatch({ //dispatch() actions
type: "ADD",
payload: inputText,
})
};
const requestDataFromServer = () => {  
return (distpatch, getState) => { //we return a function where the action itself which will be delying
//apply delay or condition based on state
// fetch(LINK)
// .then(data => { //use what we get to trigger another action, between that, there is condition check and proper delay, in a designed order
// dispatch(storeData()) //storeData is defined in reducer, will take in action which we call payload, and pass down data as payload data
// })
//we group the logic in the action here instead of in App.js
if (getState().someValue === 1){ //getState means getting the current state in the store we access to, what we get is the whole store object via getState()
dispatch(someAction())
}
}
}

Re-selectors

Selectors & Reselect for improvement enhancement

selector

  • write more reusable code of where data lives and how to derive it
  • use selectors we no longer need to destructuring the value out every time we use it
  • => the logic is more clear, straightforward, get the value from the state
export const usersSelector = (state) => state.users.users	//when we use this, we no longer needs to destructuring the value out
export const filteredUserSelector = (state) => {
return usersSelector(state).filter((user) => {
return user.includes(state.users.search);
});
}
const mapStateToProps = (state) => ({ 
// users: state.users.users //since we use selector, we do not need to type it every time we use it
users: usersSelector(state), //the logic more clear, straightforward, get the value from the state
filteredUsers: filteredUserSelector(state),
});

re-selector

  • create selectors that are memorized and only recompute when their inputs have changed.
import {createSelector} from 'reselect';	//import the library

export const usersSelector = (state) => state.users.users

export const filteredUserSelector = createSelector( //the functions are dependencies, order is important
state => state.users.users,
state => state.users.search,
(users, search) => { //functions as arguments of the dependencies
return users.filter((user) => {
return user.includes(search);
});
}
)

const mapStateToProps = (state) => ({
filteredUsers: filteredUserSelector(state),
});

Redux Basic Setup with Sagas — Code Example

index.tsx — most outer wrapper to the entire app

import { Provider } from 'react-redux';
import store from 'store';

export function Index() {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}

store/index.tsx — redux and its middleware setup

import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from "redux-saga"
import rootSaga from 'sagas/saga';

import reducer from './reducer';
const sagaMiddleware = createSagaMiddleware()

const store = configureStore({
reducer: reducer,
middleware: [sagaMiddleware]
});

sagaMiddleware.run(rootSaga)

export default store;

store/reducer.tsx — database and actions

import { combineReducers } from "redux";
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { IRootState } from 'types/ReduxState';

const DATA_INITIAL_STATE = {
data: [],
};

const LOADING_INIT_STATE = {
isLoading: false,
};

const dataReducer = createSlice({
name: 'data',
initialState: DATA_INITIAL_STATE,
reducers: {
setData(state: IRootState['data'], action: PayloadAction<any[]>) {
state.data = action.payload;
}
},
});

const loadingReducer = createSlice({
name: 'loading',
initialState: LOADING_INIT_STATE,
reducers: {
loading(state: IRootState['loading'], action: PayloadAction<boolean>) {
state.isLoading = action.payload;
},
toggleLoading(state: IRootState['loading']) {
state.isLoading = !state.isLoading;
},
},
});

export const dataActions = dataReducer.actions;
export const loadingActions = loadingReducer.actions;

const rootReducer = combineReducers({
data: dataReducer.reducer,
loading: loadingReducer.reducer,
});

export default rootReducer;

types/ReduxState.ts — typescript to define data types

type dataState = {
data: {
id: number,
name: string,
}[],
};

type loadingState = {
isLoading: boolean;
};

export interface IRootState {
data: dataState;
loading: loadingState;
}

sagas/sagas.tsx — root saga file

export default function* rootSaga() {
yield;
}

How to use it in the App?

  • useDispatch — for actions
  • useSelector — for variables
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { dataActions, loadingActions } from 'store/reducer';
import type { IRootState } from 'types/ReduxState';

export const App = () => { //Pseudocode
const dispatch = useDispatch();

const data = useSelector((state: IRootState) => state.data.data);
const isLoading = useSelector((state: IRootState) => state.loading.isLoading);

useEffect(() => {
dispatch(loadingActions.loading(true));
if (response?.data) {
dispatch(dataActions.setData(response?.data));
}
dispatch(loadingActions.loading(false));
}, [response, dispatch]);

return (
<div>
{!isLoading && data}
</div>
)
}

--

--

Hanwen Zhang

Full-Stack Software Engineer at a Healthcare Tech Company | Document My Coding Journey | Improve My Knowledge | Share Coding Concepts in a Simple Way