Redux Overview and Basic Setup with Sagas and TypeScript (Code Snippets)
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
- Single source of truth — keep all data in the store
- The state is a read-only — immutable, persistent data structure, the only way to change the state is to emit an action
- 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
andgetState
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 actionsuseSelector
— 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>
)
}