Redux
A Predictable State Container for JavaScript Apps
- Redux was inspired by several important qualities of Flux architecture
- A change emitter holding a value
- Reducers + Flux = Redux
A complex state management tool, with a single store as CDS (Central Data Store)
- To change state of the app, we call an --> Action Creator --which produces an--> Action --which is fed to--> Dispatch --which forwards the action to--> Reducers --which creates new--> State --emit a change event to all the--> Subscribers --> then, wait until we need to update the state again
Core Concepts and Principles:
- Single source of truth
- State is read-only
- Changes are made with pure reducer functions
Redux Constraints:
- Single state tree
- Actions describe updates
- Reducers apply updates
This type of data flow enables:
- Hot reloading
- Time travel
- Log actions and states (as they are just plain objects)
- Find the bad state easily, check the action, and fix the reducer
- Easy to write tests
"Redux is just a (dumb) event emitter" 🙃
Flux Architecture
Build to overcome constraints of MVC
Single direction data flow:
Action --> Dispatcher --> Store --> View
^ |
| |
| v
<---------------------------------
Store
It is the Globalized State of the application
Example:
import { createStore } from "redux";
import reducerFn from "../reducers/todosReducer";
export const store = createStore(reducerFn);
Simplified version of createStore
:
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const subscribe = (listener) => {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
};
const dispatch = (action) {
state = reducer(state, action);
listeners.forEach(listener => listener());
}
dispatch({});
return { dispatch, subscribe, getState };
};
Actions
An action must be a plain JavaScript object
Actions have 2 properties:
type
: a unique identifier (required), usually written in capital letterspayload
: data (it is optional)
Example:
// action creator
const createAdd = (num) => {
// returns an action
return {
type: "ADD_ONE",
payload: {
addBy: num,
},
};
};
- To use
async
calls use a middleware such asredux-thunk
orredux-saga
Reducers
Manages the state and returns the newly updated state
- Reducers must be Pure Functions
- Should not mutate the original state (no side-effects)
- Always provide a default state
- Must return any value besides
undefined
- Should always return new state
- They should be synchronous functions
Example:
const defaultState = {
counter: 0,
};
const reducerFn = (state = defaultState, action = {}) => {
switch (action.type) {
case "INC":
return { counter: state.counter + 1 };
case "DEC":
return { counter: state.counter - 1 };
case "ADDBY":
return { counter: state.counter + action.payload };
default:
return state;
}
};
- Combine multiple reducers
import { createStore, combineReducers } from "redux";
const allReducers = combineReducers({
reducerFn,
// ...
});
const store = createStore(allReducers);
Dispatch
Dispatch is used to send actions to update the data
Example:
store.dispatch(createAdd(25));
- Using Hooks:
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch({ type: "INC" });
dispatch({ type: "DEC" });
dispatch({ type: "ADDBY", payload: 10 });
Using Redux
Simple Redux store used in a simple JavaScript app:
<p id="value"></p>
<button id="increment">Increment</button>
import { createStore } from "redux";
const valueEl = document.getElementById("value");
// 1. Create reducer function
const counterReducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { counter: state.counter + 1 };
default:
return state;
}
};
// 2. Set initial state
const initialState = { counter: 0 };
// 3. Create a store
const store = createStore(counterReducer, initialState);
// 5. When the subscription callback runs:
const render = () => {
// 5.1 Get the current store state
const state = store.getState();
// 5.2 Extract the data you want
const newValue = state.counter.toString();
// 5.3 Update the UI with the new value
valueEl.innerHTML = newValue;
};
// 4. Subscribe to store updates
store.subscribe(render);
// 6. Display the UI with the initial store state
render();
// 7. Dispatch actions based on UI inputs
document.getElementById("increment").addEventListener("click", () => {
store.dispatch({ type: "INCREMENT" });
});
react-redux
react-redux
subscribes to the Redux store, checks to see if the data your component wants has changed, and re-renders your component
It provides bind between React and Redux
Added Hooks API in v7.1
Integrating Redux in a React App:
Install Redux and
react-redux
:bashnpm install redux react-redux # or yarn add redux react-redux
redux-toolkit
Includes packages like:
- Redux (core library, state management)
- Immer (allows to mutate state)
redux-thunk
(handles async actions)- reselect (simplifies reducer functions)
Usage:
Install the required packages:
bashnpm install @reduxjs/toolkit react-redux # Or yarn add @reduxjs/toolkit react-redux
createSlice
:javascriptimport { createSlice } from "@reduxjs/toolkit"; const initialState = { value: 0, }; export const counterSlice = createSlice({ name: "counter", initialState reducers: { increment:(state) => { state.value += 1; }, decrement:(state) => { state.value -= 1; }, incrementByAmount:(state, action) => { state.value += action.payload; }, }, });
configureStore
:- Simplified configuration options and good defaults
- Includes
redux-thunk
middleware - Enables Redux DevTools Extension
javascriptimport { configureStore } from "@reduxjs/toolkit"; export const store = configureStore({ reducer: { counter: counterSlice.reducer, }, });
Export Actions:
javascript// returns an action //{ type: "UNIQUE KEY"} counterSlice.actions.increment; // action creators are generated for each case reducer function export const { increment, decrement, incrementByAmount } = counterSlice.actions;
Dispatch actions:
javascriptimport { useDispatch, useSelector } from "react-redux"; const APP = () => { const counter = useSelector((state) => state.counter.value); const dispatch = useDispatch(); const incrementHandler = () => { dispatch(counterSlice.actions.increment()); }; return <h1>Counter: {counter}</h1>; };
Redux Middleware
Middleware provides a 3rd-party extension point between dispatching an action and the moment it reaches the reducer. (For logging, crash reporting, async tasks, etc.)
redux-thunk
Middleware to help us write async actions
The word "thunk" is a programming term that means "a piece of code that does some delayed work".
Add
redux-thunk
as middleware:javascriptimport { applyMiddleware, createStore } from "redux"; import thunk from "redux-thunk"; const store = createStore(reducers, applyMiddleware(thunk));
Async action creators must return a function, that takes
dispatch
andgetState
(optional) as parameters. This function should manually calldispatch
javascriptexport const fetchUser = (id) => async (dispatch) => { const res = await jsonPlaceholder.get(`/users/${id}`); dispatch({ type: "FETCH_USER", payload: res.data, }); };
Rest of the Redux config will be same
redux-saga
Watcher Saga --> Actions --> Worker Saga