You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Redux and React-Redux Design and Implementation: This article provides a deep dive into the design and implementation of Redux and React-Redux, demonstrating how they manage application state and facilitate communication between components.
State Management (Getter, Setter): You will understand the fundamental pattern of state management and be able to understand any other state management tools.
Publish/Subscribe Design Pattern: This article explains the publish/subscribe pattern, a key concept in Redux's state update and notification mechanism.
What is Redux?
Redux is a predictable state container for JavaScript apps. It's like a more powerful version of React's state. While React's state is limited to each component, Redux allows you to manage the state of your entire application in one place.
Redux solves this problem by storing the state of your entire application in a single JavaScript object within a single store. This makes it easier to track changes over time, debug, and even persist the state to local storage and restore it on the next page load.
Redux contains these several components:
Action: A plain object describing what happened and the changes to be made to the state.
Dispatcher: A function that takes an action object and sends it to the store to change the state.
Store: The central repository that holds the state of the application. It allows access to the state, dispatching actions, and registering listeners.
View: The user interface that displays the data provided by the store. It can trigger actions based on user interactions.
If some action on the application, for example pushing a button causes the need to change the state, the change is made with an action. This causes re-rendering of the view.
Let's take a look at the implementation of a counter:
The impact of the action on the state of the application is defined using a reducer. In practice, a reducer is a function that is given the current state and an action as parameters. It returns to a new state.
Let's now define a reducer for our application:
// the first state is the current state in store, and the function return// a new state after action.constcounterReducer=(state,action)=>{if(action.type==='INCREMENT'){returnstate+1;}elseif(action.type==='DECREMENT'){returnstate-1;}elseif(action.type==='ZERO'){return0;}returnstate;}
And an action is like this:
{type: 'INCREMENT',}
With the reducer, we can use redux to define a store.
import{createStore}from'redux';// initial state is 0constcounterReducer=(state=0,action)=>{// ...};conststore=createStore(counterReducer);
A store has two core methods: dispatch, subscribe. A function can be subscribe to a store, and a dispatch takes an action and changes the state, when the state is changed, the functions that subscribe to the store will be called.
redux is following Publish/Subscribe design pattern. Where store is a channel from subscribing and publishing messages. The dispatch method is used to publish messages to the store. When an message is dispatched, the state of the application is changed. And subscribe method allows functions (subscribers) to subscribe to the store. These subscribers are notified when the state changes due to dispatched messages.
Here are some benefits of Publish/Subscribe Pattern:
Loose coupling between components: the component that publish something doesn't need to who subscribe to the channel, making the system more modular and flexible.
High scalability (in theory, Pub/Sub allows any number of publishers to communicate with any number of subscribers).
As we can see, the core of redux is the function createStore, and the dispatch, subscribe methods of a store. We will skip other methods first and implement these functions.
We first define their interfaces:
interfaceAction{type: string;[extraProps: string]: any;}interfaceReducer<T>{(state: T,action: Action): T;}// a store should have dispatch, subscribe methodsinterfaceStore<T>{dispatch: (action: Action)=>void;// settersubscribe: (listener: ()=>void)=>void;getState: ()=>T;// getter}// accept a reducer and initial state, return a store objectfunctioncreateStore<T>(reducer: Reducer<T>,initialState?: T): Store<T>;// create
The first step, we implement the storage logic and the basic framework:
functioncreateStore<T>(reducer: Reducer<T>,initialState?: T): Store<T>{// store reduce and state to local variableletcurrentReducer=reducer;letcurrentState: T|undefined=initialState;// store listen(function that subscribe to this store) in a map id -> funcletcurrentListeners: Map<number,ListenerCallback>|null=newMap();// a id that will assign to the listenerletlistenerIdCounter=0;functionsubscribe(listener: ()=>void){// store listenerconstlistenerId=listenerIdCounter++;currentListeners.set(listenerId,listener);}functiondispatch(action: Action){}functiongetState(){}return{
dispatch,
subscribe,
getState,}}
And then we will start implement dispatch logic. When dispatch an action, the current state will change, and it will call all listeners subsequently.
functioncreateStore<T>(reducer: Reducer<T>,initialState?: T): Store<T>{// store reduce and state to local variableletcurrentReducer=reducer;letcurrentState: T|undefined=initialState;// store listen(function that subscribe to this store) in a map id -> funcletcurrentListeners: Map<number,ListenerCallback>|null=newMap();// a id that will assign to the listenerletlistenerIdCounter=0;functionsubscribe(listener: ()=>void){// store listenerconstlistenerId=listenerIdCounter++;currentListeners.set(listenerId,listener);}functiondispatch(action: Action){// We will call reducer using action and current state, and update current statecurrentState=currentReducer(currentState,action);// Call all listener one by one after update the statecurrentListeners.forEach(listener=>{listener();});}functiongetState(){returncurrentState;}return{
dispatch,
subscribe,
getState,}}
Now our toy redux is done. Notice that this is a simplified system without any error handling, if you take a look at redux's source code you shall see almost half of the code is handling error.
Introduce React-Redux
So the front part of redux flow is done, we can now: dispatch an action -> store state update. But how to update the view? We need to introduce react-redux, React Redux provides a pair of custom React hooks that allow your React components to interact with the Redux store.
useSelector reads a value from the store state and subscribes to updates(getter) the view, while useDispatch returns the store's dispatch method to let you dispatch actions(setter). When dispatch something, the propagation happens and informs all components with useSelector to update their values.
The core principle of react-redux is propagation. propagation represent the process that when there is a state changed, it will inform the root node about the change and the root node will carry the information to its children nodes, and thus the information is propagation through the whole tree.
We will create a Subscription interface, which contains:
exportinterfaceSubscription{// add children subscription, thus a tree structure is formedaddNestedSub: (listener: VoidFunc)=>VoidFunc// propagate the information to its children nodesnotifyNestedSubs: VoidFunc// check whether the node is subscribedisSubscribed: ()=>boolean// do something when the node is notifiedhandleChangeWrapper: VoidFunc// this is for component to attach their function to this node so that handleChangeWrapper can call itonStateChange?: VoidFunc|null// try to subscribe to a storetrySubscribe: VoidFunc// unsubscribe the node for garbage collectiontryUnsubscribe: VoidFunc}
interfaceListenerCollection{notify: ()=>void;subscribe: (callback: ()=>void)=>void;unsubscribe:
}[]exportfunctioncreateSubscription(store: any,parentSub?: Subscription){// for store unsubscribe functionletunsubscribe: VoidFunc|undefined;// for store the listeners// for simplification we don't implement the listener methods here, we will do some wishful thinking and assume that we have a ListenerCollection class which is a link list that stores all the listeners(callback)letlisteners: ListenerCollection;// Is this specific subscription subscribed (or only nested ones?)letselfSubscribed=false;functionisSubscribed(){returnselfSubscribed;}functiontrySubscribe(){// if it is a root node, subscribe its method to store// else add to parent's listenersif(!unsubscribe){unsubscribe=parentSub
? parentSub.addNestedSub(handleChangeWrapper)
: store.subscribe(handleChangeWrapper);// create a empty link list of listener for preparing a space for its children nodeslisteners=createListenerCollection();}}functiontryUnsubscribe(){if(unsubscribe){// call unsubscribe method, unsubscribe from store or parent listenersunsubscribe();unsubscribe=undefined;// clear its listenerslisteners.clear();listeners=null;}}// for children nodes to add their listener to parent nodefunctionaddNestedSub(listener: ()=>void){constcleanupListener=listeners.subscribe(listener);return()=>{// unsubscribetryUnsubscribe();// clear its listenerscleanupListener();};}// force rerenderfunctionhandleChangeWrapper(){subscription.onStateChange();}// propagate changefunctionnotifyNestedSubs(){listeners.notify()}constsubscription: Subscription={
addNestedSub,
notifyNestedSubs,
handleChangeWrapper,
isSubscribed,
trySubscribe,
tryUnsubscribe,};returnsubscription;}
Now we have a way to create subscriptions that is associated with a certain store. The basic flow is:
Call createSubscription with store and call trySubscribe to get a root subscription, we can also assign a callback to onStateChange.
Call createSubscription with store and root to get a child subscription, when call trySubscribe, instead of binding the onStateChange to store, it will be added to root listeners list.
When store is changed, onStateChange of root will be called, and root will call notifyNestedSubs to notify children subscriptions, and the children subscriptions will do the same thing and notify their children subscriptions recursively. Thus, all nodes in the tree is informed. This process is called propagation.
Provider
First of all, we need to store our state in somewhere, react-redux use Context to store.
We first take a look at how to implement Provider, which create and injects the store and a subscription to the children components.
constReactReduxContext=React.createContext(null);functionProvider({store, children}){// provider will create a subscription when store with root is changed.constcontextValue=useMemo(()=>{constsubscription=newSubscription(store);subscription.onStateChange=subscription.notifyNestedSubs;return{
store,
subscription,};},[store]);// get initial stateconstpreviousState=useMemo(()=>store.getState(),[store]);// when previousState or contextValue change, try to subscribe againuseLayoutEffect(()=>{const{ subscription }=contextValue// subscribe to new storesubscription.trySubscribe()// if the state is changed, notify listenersif(previousState!==store.getState()){subscription.notifyNestedSubs()}return()=>{subscription.tryUnsubscribe()subscription.onStateChange=null}},[contextValue,previousState])return(// inject subscription and store to children nodes<ReactReduxContext.Providervalue={contextValue}>{children}</ReactReduxContext.Provider>);}
useSelector
Now we have a way to get store and root subscription in children components, we start implement a useSelector hook, which add a subscription to the subscription tree, and force component to re-render when state is changed.
Here we use useReducer for telling the component to rerender.
functionMyComponent(){// when dispatch is call, the component will rerenderconst[state,dispatch]=useReducer((s)=>s+1);return// ...}
Then we implement a simplified useSelector hook:
// whenever the state in store is changed, update the state and inform a rerender.// a selector callback for filter the state we want, equalityFn is for compare state change, here we use strictly equal ===functionuseSelector(selector,equalityFn=(a,b)=>a===b){// get store and root subscription from contextconst{ store,subscription: contextSub}=useContext(ReactReduxContext);// utilize the forceRender function for rerenderconst[,forceRender]=useReducer((s)=>s+1,0);// create a new subscription for the component that call this hook with root subscriptionconstsubscription=useMemo(()=>newSubscription(store,contextSub),[store,contextSub,]);// get current state when re-renderconststoreState=store.getState();// store selected stateletselectedState;// cache selector, store state, selected state when every time renderconstlatestSelector=useRef();constlatestStoreState=useRef();constlatestSelectedState=useRef();useLayoutEffect(()=>{latestSelector.current=selector;latestStoreState.current=storeState;latestSelectedState.current=selectedState;});// if the cache is needed to updateif(selector!==latestSelector.current||storeState!==latestStoreState.current){// calculate new selected stateconstnewSelectedState=selector(storeState);// if new selected state is not equal to the previous stateif(latestSelectedState.current===undefined||!equalityFn(newSelectedState,latestSelectedState.current)){// update stateselectedState=newSelectedState}else{// use previous stateselectedState=latestSelectedState.current}}else{// use previous stateselectedState=latestSelectedState.current}// attach checkForUpdates to the subscription's onStateChange//Every time subscriptions are updated, checkForUpdates will be calleduseLayoutEffect(()=>{// compare store state with current state// if it is not equal, update current state// force re-render the componentfunctioncheckForUpdates(){try{constnewStoreState=store.getState();constnewSelectedState=latestSelector.current(newStoreState);if(equalityFn(newSelectedState,latestSelectedState.current)){return;}latestSelectedState.current=newSelectedState;latestStoreState.current=newStoreState;}catch(err){}// re-render anywayforceRender();}// attach checkForUpdates to the subscription's onStateChangesubscription.onStateChange=checkForUpdates;subscription.trySubscribe();// call checkForUpdates for initializationcheckForUpdates();return()=>subscription.tryUnsubscribe();},[store,subscription]);// return state we wantreturnselectedState;}
useReducer
As you can see we use useReducer here to trigger a state update and trigger a render. You might wonder why we don't use useState to do the same thing. The useState indeed can be used to force a re-render, but it requires writing extra code.
What You Will Learn From This Article
What is Redux?
Redux is a predictable state container for JavaScript apps. It's like a more powerful version of React's state. While React's state is limited to each component, Redux allows you to manage the state of your entire application in one place.
Redux solves this problem by storing the state of your entire application in a single JavaScript object within a single store. This makes it easier to track changes over time, debug, and even persist the state to local storage and restore it on the next page load.
Redux contains these several components:
If some action on the application, for example pushing a button causes the need to change the state, the change is made with an action. This causes re-rendering of the view.
Let's take a look at the implementation of a counter:
The impact of the
action
on the state of the application is defined using areducer
. In practice, areducer
is a function that is given the current state and an action as parameters. It returns to a new state.Let's now define a
reducer
for our application:And an
action
is like this:With the
reducer
, we can useredux
to define astore
.A
store
has two core methods:dispatch
,subscribe
. A function can besubscribe
to a store, and adispatch
takes an action and changes the state, when the state is changed, the functions thatsubscribe
to the store will be called.Publish/Subscribe Pattern
redux
is following Publish/Subscribe design pattern. Where store is a channel from subscribing and publishing messages. The dispatch method is used to publish messages to the store. When an message is dispatched, the state of the application is changed. And subscribe method allows functions (subscribers) to subscribe to the store. These subscribers are notified when the state changes due to dispatched messages.Here are some benefits of
Publish/Subscribe
Pattern:Loose coupling between components: the component that
publish
something doesn't need to whosubscribe
to the channel, making the system more modular and flexible.High scalability (in theory, Pub/Sub allows any number of publishers to communicate with any number of subscribers).
Redux Implementation
As we can see, the core of
redux
is the functioncreateStore
, and thedispatch
,subscribe
methods of a store. We will skip other methods first and implement these functions.We first define their interfaces:
The first step, we implement the storage logic and the basic framework:
And then we will start implement dispatch logic. When dispatch an action, the current state will change, and it will call all listeners subsequently.
Now our toy redux is done. Notice that this is a simplified system without any error handling, if you take a look at redux's source code you shall see almost half of the code is handling error.
Introduce React-Redux
So the front part of redux flow is done, we can now: dispatch an action -> store state update. But how to update the view? We need to introduce
react-redux
, React Redux provides a pair of custom React hooks that allow your React components to interact with the Redux store.useSelector
reads a value from the store state and subscribes to updates(getter) the view, whileuseDispatch
returns the store'sdispatch
method to let you dispatch actions(setter). Whendispatch
something, the propagation happens and informs all components withuseSelector
to update their values.Subscription(Propagation)
The core principle of
react-redux
ispropagation
.propagation
represent the process that when there is a state changed, it will inform the root node about the change and the root node will carry the information to its children nodes, and thus the information is propagation through the whole tree.We will create a Subscription interface, which contains:
Now we have a way to create subscriptions that is associated with a certain
store
. The basic flow is:createSubscription
withstore
and calltrySubscribe
to get aroot
subscription, we can also assign a callback toonStateChange
.createSubscription
withstore
androot
to get a child subscription, when calltrySubscribe
, instead of binding theonStateChange
tostore
, it will be added toroot
listeners list.store
is changed,onStateChange
ofroot
will be called, androot
will callnotifyNestedSubs
to notify children subscriptions, and the children subscriptions will do the same thing and notify their children subscriptions recursively. Thus, all nodes in the tree is informed. This process is calledpropagation
.Provider
First of all, we need to store our state in somewhere,
react-redux
useContext
to store.Here is a simple
Context
example:And the components that are child node of Provider can access
CounterContext
via auseContext
:We first take a look at how to implement
Provider
, which create and injects thestore
and asubscription
to the children components.useSelector
Now we have a way to get
store
and root subscription in children components, we start implement auseSelector
hook, which add a subscription to the subscription tree, and force component to re-render when state is changed.Here we use
useReducer
for telling the component torerender
.Then we implement a simplified
useSelector
hook:useReducer
As you can see we use
useReducer
here to trigger a state update and trigger arender
. You might wonder why we don't useuseState
to do the same thing. TheuseState
indeed can be used to force a re-render, but it requires writing extra code.useDispatch
useDispatch
is relatively simple, it get thedispatch
function from the context:The text was updated successfully, but these errors were encountered: