“Content” : This article briefly analyzes the principle and realization of Redux & Redux ecology. “Require” : Some experience with Redux is required to understand this article. “Gain” : you will Gain

  1. Write Redux, knowing exactly what you’re doing and what the impact of each line of code is.
  2. Understand how storeEnhancer Middleware works and make your own depending on your needs.
  3. Learning about functional paradigms is a great example of how they can be applied in practice.

“Correction” : If there are any mistakes, you are welcome to make comments and feedback

Redux design philosophy

Single source of truth

There can be only one unique global data source, and there is a one-to-one correspondence between state and view


Data – View Mapping


State is read-only

The state is read-only, and when we need to change it, we replace it with a new one, rather than making changes directly to the original data.

Changes are made with pure functions

State updates are done using a Reducer function, which accepts an object (Action) that describes how the state changed to create the new state.


State Change



Single State + Pure Function


Redux architecture

Redux components

  • State: Global state object, unique and immutable.
  • Store: An object generated by calling the createStore function that encapsulates the methods defined within the createStore to manipulate global state and used by the user to use Redux.
  • Action: An object that describes how the state is changed, has a fixed type property, and is submitted through the Store’s Dispatch method.
  • Reducer: A pure function that actually performs state modification. Reducer is defined and passed in by the user and received from dispatch

Action takes the argument, evaluates to return the new state, completes the state update, and then executes the subscribed listener.

  • StoreEnhancer: A higher-order encapsulation of createStore to enhance store capabilities. ApplyMiddleware provided by Redux is an official implementation of storeEnhancer.
  • Middleware: A higher-order functional encapsulation of dispatch, in which applyMiddleware replaces the dispatch with an implementation containing the middleware chain calls.

Redux constitute

Redux API implementation

Redux Core

createStore

CreateStore is a large closure environment that defines the Store itself, as well as its various apis. There are signs in the environment to detect side effects such as obtaining state, triggering dispatch, change monitoring, etc., so Reducer is strictly controlled as a pure function. All the core ideas of the Redux design are implemented in this file. The entire file is only over 300 lines, which is simple but important. Here is a brief list of the functions implemented in this closure and the source code analysis to strengthen your understanding.

StoreEnhancer is applied if there is one

if (typeofenhancer ! = ='undefined') {
// Type detection
if (typeofenhancer ! = ='function') {... }// enhancer Accept a storeCreator return a storeCreator
// When it is applied, it returns storeCreatetor directly and returns the corresponding store
return enhancer(createStore)(reducer,preloadedState)
}
Copy the code

Otherwise dispatch an INIT action to make the Reducer produce an initial state. Note that INIT is a random number defined internally by Redux, which cannot be clearly defined by Reducer, and the state may be undefined at this time, so the reducer can successfully complete the initialization. We need to follow the following two rules when compiling reducer:

  1. For actions that do not define type, return the state of the input parameter.
  2. CreateStore If no initial state was passed in, the reducer must provide a default value.
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)
Copy the code

Finally, the methods defined in the closure are loaded into the Store object and returned

const store = {
dispatch,
subscribe,
getState,
replaceReducer, // Not used, so skip it
[$$observable]: observable // Not used, so skip it
}
return store;
Copy the code

Here is how these methods are implemented

getState

It is specified that getState cannot be called in reducer, and the current state is returned if conditions are met. It is very clear and will not be described again.

function getState(){
if (isDispatching) {
    ...
}
return currentState
}
Copy the code

dispatch

The built-in Dispatch only provides support for regular object actions, with other support like AsyncAction being put into middleware. Dispatch does two things:

  1. The call to Reducer produces a new state.
  2. Call the subscribed listener function.
For an ordinary Object, its prototype is Object */
function isPlainObject(obj){
    if (typeofobj ! = ='object' || obj === null) return false
    let proto = obj
    // proto out of the loop is Object
    while (Object.getPrototypeOf(proto) ! = =null) {
        proto = Object.getPrototypeOf(proto)
    }
    return Object.getPrototypeOf(obj) === proto
}
function dispatch(action: A) {
    // Check if it is a normal object
    if(! isPlainObject(action)) { ... }// Redux requires action to have a type attribute
    if (typeof action.type === 'undefined') {... }Do not use this tool in reducer
    if (isDispatching) {
        ...
    }
    Call Reducer to produce a new state and replace the current state
    try {
        isDispatching = true
        currentState = currentReducer(currentState, action)
    } finally {
        isDispatching = false
    }
    // Call the subscription listener
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }
    return action
}
Copy the code

subscribe

Subscribe status updates and return unsubscribe methods. In fact, as long as dispatch is called, even if the reducer does not make any changes to the state, the listening function will also be triggered. Therefore, in order to reduce rendering, state diff will be made in the listener registered by itself in each UI Bindings to optimize performance. Note that the listener allows side effects.

// Make the nextListeners a slice of the currentListeners and then modify the slice to replace the currentListeners
function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice()
    }
}
function subscribe(listener: () => void) {
    // Type detection
    if(typeoflistener ! = ='function'){
        ...
    }
    No subscriptions are allowed in the reducer
    if (isDispatching) {
        ...
    }
    let isSubscribed = true
    ensureCanMutateNextListeners()
    nextListeners.push(listener)
    return function unsubscribe() {
    // Prevent double unsubscribing
    if(! isSubscribed) {return
    }
    Unsubscription is also not allowed in the Reducer
    if (isDispatching) {
        ...
    }
    isSubscribed = false
    ensureCanMutateNextListeners()
    const index = nextListeners.indexOf(listener)
    nextListeners.splice(index, 1)
    currentListeners = null}}Copy the code

applyMiddleware

ApplyMiddleware is an official implementation of storeEnhance to give Redux plug-in capabilities for various actions.

storeEnhancer

As you can see from the function signature, this is a higher-order function encapsulation of createStore.

type StoreEnhancer = (next: StoreCreator) = >StoreCreator;Copy the code

The CreateStore entry accepts only one storeEnhancer. If more than one storeEnhancer is passed in, compose is used to compose them. This is crucial to understanding how middleware works in chain calls, so look at that section first.

middleware

type MiddlewareAPI = { dispatch: Dispatch, getState: () = > State } 
type Middleware = (api: MiddlewareAPI) = > (next: Dispatch) = > Dispatch 
Copy the code

The outermost function receives the middlewareApi, provides the Store portion of the MIDDLEWARE API, and returns a function that participates in compose to make a chain call to middleware.

export default function applyMiddleware(. middlewares) {
    return (createStore) = >{             
            // Initialize store and get dispatch
            const store = createStore(reducer, preloadedState)             
            // Dispath is not allowed in Middlware
            let dispatch: Dispatch = () = > {                 
                throw new Error(                     
                'Dispatching while constructing your middleware is not allowed. ' +                     'Other middleware would not be applied to this dispatch.')}const middlewareAPI: MiddlewareAPI = {
                getState: store.getState,                 
                dispatch: (action, ... args) = >dispatch(action, ... args) }// Inject the API into Middlware
            const chain = middlewares.map(middleware= > middleware(middlewareAPI))                 // Focus on understanding
            // compose passes into dispatch, generating a new chain of layered dispath calls
            dispatch = compose<typeofdispatch>(... chain)(store.dispatch)// Replace dispath, return
            return {                 
                ...store,                 
                dispatch             
            }         
        } 
} 
Copy the code

Another example of middleware: Redux-thunk enables Redux to support asyncAction, which is often used in asynchronous scenarios.

// The outermost layer is a middleware factory function that generates middleware and injects additional parameters into asyncAction
function createThunkMiddleware(extraArgument) {   
    return ({ dispatch, getState }) = > (next) = > 
        (action) = > {     
        // Determine the type of action in the middleware. If it is a function, execute it directly
        if (typeof action === 'function') {       
            return action(dispatch, getState, extraArgument);     }     // Otherwise continue
            return next(action);   
    };
} 
Copy the code

Redux Utils

compose

Compose is a process often used in the functional programming paradigm, which creates a right-to-left flow of data, with the results of the right-side function execution passed as arguments to the left. Compose is a higher-order function that takes n function arguments and returns a function executed in the above data stream. If the array of arguments is also a higher-order function, the result of its compose function will be something like the following. The function returned by the array of higher-order functions will be a chain call from left to right.

export default function compose(. funcs) {
    if (funcs.length === 0) {
        return (arg) = > arg
    }
    if (funcs.length === 1) {
        return funcs[0]}// simple direct compose
    return funcs.reduce(
        (a, b) = >
            (. args: any) = >a(b(... args)) ) }Copy the code

combineReducers

It’s also a combination, but it’s a tree-like combination. You can create a complex Reducer, as shown in the following figure

The implementation method is also relatively simple, that is, the map object with a function package layer, return a mapedReducer, the following is a simplified implementation.

function combineReducers(reducers){      
    const reducerKeys = Object.keys(reducers)      
    const finalReducers = {}      
    for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]          
        finalReducers[key] = reducers[key]
    }     
    const finalReducerKeys = Object.keys(finalReducers)     
    // Reducer
    return function combination(state, action){         
        let hasChanged = false         
        const nextState = {}         
        // Iterate over and execute
        for (let i = 0; i < finalReducerKeys.length; i++) {           
            const key = finalReducerKeys[i]           
            const reducer = finalReducers[key]           
            const previousStateForKey = state[key]           
            const nextStateForKey = reducer(previousStateForKey, action)           
            if (typeof nextStateForKey === 'undefined') {... } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length         
        return hasChanged ? nextState : state         
        }     
    } 
} 
Copy the code

bindActionCreators

Create an Action with actionCreator and dispatch it immediately

function bindActionCreator(ActionCreator, dispatch) {   
    return function (this. args) {     
        return dispatch(actionCreator.apply(this, args))
    }
} 
Copy the code

Redux UI bindings

React-redux

React- Redux is the official redux UI Bindings implementation. It provides two ways to use Redux: HOC and Hooks, corresponding to Class and functional components, respectively. We chose its Hooks implementation for analysis, focusing on how the UI component gets the global state and notifies the UI of updates when the global state changes.

How does the UI get global state

  1. React Context stores global state
export const ReactReduxContext =   /*#__PURE__*/ 
React.createContext<ReactReduxContextValue | null> (null) 
Copy the code
  1. Encapsulate it as a Provider component
function Provider({ store, context, children }: ProviderProps) {     
    const Context = context || ReactReduxContext     
    return <Context.Provider value={contextValue}>{children}</Context.Provider>  
} 
Copy the code
  1. Provide hook to get store: useStore
function useStore(){     
    const { store } = useReduxContext()!      
    return store 
} 
Copy the code

How do I notify UI updates when State changes

React-redux provides a hook: useSelector. This hook subscribes a listener to redux that is triggered when the state changes. It mainly does the following things.

When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.

  1. subscribe
  const subscription = useMemo(
  () = > createSubscription(store),     
  [store, contextSub]
  )   
  subscription.onStateChange = checkForUpdates 
Copy the code
  1. state diff
    function checkForUpdates() {       
        try {         
            const newStoreState = store.getState()         
            constnewSelectedState = latestSelector.current! (newStoreState)if (equalityFn(newSelectedState, latestSelectedState.current)) {           
                return         
            }          
            latestSelectedState.current = newSelectedState         
            latestStoreState.current = newStoreState       
        } catch (err) {        
            // we ignore all errors here, since when the component
            // is re-rendered, the selectors are called again, and
            // will throw again, if neither props nor store state
            // changed         
            latestSubscriptionCallbackError.current = err as Error       
        }        
        forceRender()    
    } 
Copy the code
  1. re-render
 const [, forceRender] = useReducer((s) = > s + 1.0)  
 forceRender() 
Copy the code

How to use REdux without UI Bindings

In fact, just complete the above three steps can be used, here is an example:

const App = () = >{     
const state = store.getState();     
const [, forceRender] = useReducer(c= >c+1.0);      
// Subscribe to updates, state changes refresh component
useEffect(() = >{         
    // Unsubscribe when the component is destroyed
    return store.subscribe(() = >{             
    forceRender();         
    });    
},[]);      
const onIncrement = () = > {         
    store.dispatch({type: 'increment'});     
};     
const onDecrement = () = > {         
    store.dispatch({type: 'decrement'});     
}         
return (
<div style={{textAlign:'center', marginTop:'35'}} % >
    <h1 style={{color: 'green', fontSize: '500'}} % >{state.count}</h1>
    <button onClick={onDecrement} style={{marginRight: '10'}} % >decrement</button>
    <button onClick={onIncrement}>increment</button>         
</div>)}Copy the code

summary

The Redux core simply implements its “single state”, “state immutable”, “pure function” Settings, and is very small. StoreEnhancer and Middleware are exposed to add functionality to this concept and enrich the ecosystem. The development of Redux also proves that such design ideas make Redux very scalable. Among them, the application of higher-order functions is a plug-in system construction method that I think is worth learning from. Instead of directly setting the life cycle, the core functions are directly packaged with higher-order functions, and then the internal dependence on compose to complete the chain call can reduce the development mentality of external developers. The problem Redux wants to solve is the mapping problem of complex state and view, but Redux itself has no direct implementation, it only does the state management, and then exposes the monitoring ability of state update, the remaining state cache, state comparison, update view is left to the UI-Bindings of various frameworks, This keeps their code simple and stable while providing a “responsive class” state-view development experience for developers who actually use Redux State management at the View layer.

❤️ Thank you

That is all the content of this sharing. I hope it will help you

Don’t forget to share, like and bookmark your favorite things.

Welcome to pay attention to the public number ELab team receiving factory good article ~

We are from the front end department of Bytedance, responsible for the front end development of all bytedance education products.

We focus on product quality improvement, development efficiency, creativity and cutting-edge technology and other aspects of precipitation and dissemination of professional knowledge and cases, to contribute experience value to the industry. Including but not limited to performance monitoring, component library, multi-terminal technology, Serverless, visual construction, audio and video, artificial intelligence, product design and marketing, etc.

Interested students are welcome to post in the comments section or use the internal tweet code to the author’s section at 🤪

Bytedance enrollment internal push code: YCE7SSZ

Post links: jobs.toutiao.com/s/d7sPTXu