A preface

Hello, I’m 👽. We’re going to publish a new series on React V18, which will focus on the background, features, and principles of the new features.

UseMutableSource’s original RFC proposal began in February 2020. It will be a new feature in React 18. Summarize useMutableSource with a description from the proposal.

UseMutableSource enables React components to safely and efficiently read external data sources in Concurrent Mode, detect changes during component rendering, and schedule updates when data sources change.

When it comes to external data sources, we need to start with state and update. Both React and Vue traditional UI frameworks adopt the virtual DOM approach, but they still cannot delegate update units to the virtual DOM. Therefore, the minimum granularity of update is still at the component level. The component manages data state uniformly and participates in scheduling updates.

Back to our React hero, since the component controls state. In V17 and previous versions, React wanted updates on views only by changing the internal data state. The React update mode is not dependent on its own state. Let’s take a look at the React update modes.

  • The component itself changes state. functionuseState | useReducerThat kind of componentsetState | forceUpdate
  • propsChange, the update of a child component brought about by a component update.
  • contextUpdate, and the component consumes the currentcontext

Either way, it’s essentially a change of state.

  • propsChanges from the parent componentstateChange.
  • contextThe change comes from the change of value in the Provider, and value is generally state or a derivative of state.

Model => View But state is limited to data inside the component, if state comes from outside (outside the component level). How do you convert an external data source to an internal state, change the data source, and re-render the component?

Normal mode, the external Data first external Data through the selector selector of Data mapping component needs to state | props. This completes the process, and then you subscribe to changes in the external data source and force forceUpdate itself if any changes occur. The following two graphs represent data injection and data subscription updates.

The typical external data source is the store in Redux, and how Redux safely converts the state in the store to the state in the component.

Perhaps I could use a piece of code to represent the flow from state changes to view updates in React-Redux.

const store = createStore(reducer,initState)

function App({ selector }){
    const [ state , setReduxState ] = React.useState({})
    const contextValue = useMemo(() = >{
        /* Subscription store changes */
        store.subscribe(() = >{
             /* Select the subscription state */ with the selector
             const value = selector(data.getState())
             /* If there are changes */
             if(ifHasChange(state,value)){
                 setReduxState(value)
             }
        })
    },[ store ])    
    return <div>.</div>
}
Copy the code

But the code in this example, it’s not really meaningful, it’s not source code, so I’m just going to give you a clear idea of the flow. Redux and React essentially work like this.

  • Subscribe to state changes with store.subscribe, but it’s essentially more complicated than in the snippet, finding the required state for the component using a selector. Let me explain it hereselectorIn this case, you need to select ‘useful’ from state and merge it with props. The selectors need to be supported by andreact-reduxConnect the first parameter mapStateToProps For the details, it doesn’t matter, because the point today isuseMutableSource.

In the absence of useMutableSource, the subscription to update process is no longer handed over to the component. As follows:

/* Create store */
const store = createStore(reducer,initState)
/* Create external data source */
const externalDataSource = createMutableSource( store ,store.getState() )
/* Subscribe to update */
const subscribe = (store, callback) = > store.subscribe(callback);
function App({ selector }){
    /* If the state of the subscription changes, the component is updated */
    const state = useMutableSource(externalDataSource,selector,subscribe)
}
Copy the code
  • Create external data sources through createMutableSource and use external data sources through useMutableSource. The external data source changes and the component is automatically rendered.

As shown above, subscription updates are implemented via useMutableSource, which reduces in-app component code, improves code robustness, and reduces coupling to some extent. Let’s take a look at all the new V18 features.

2 Function Introduction

For details, refer to the latest RFC, createMutableSource and useMutableSource. To some extent, they are similar to createContext and useContext. The difference is that the context requires the Provider to inject internal state, whereas today’s focus is on injecting external state. So we should first look at how both are used.

create

CreateMutableSource creates a data source. It takes two arguments:

const externalDataSource = createMutableSource( store ,store.getState() ) 
Copy the code
  • The first parameter is an external data source, such as a store in Redux,
  • The second parameter: a function, the return value of the function as the version number of the data source, note here ⚠️, to maintain the consistency of the data source and the data version number, that is, the data source changes, then the data version number must change, to some extent followimmutablePrinciple (immutability). It can be understood that the data version number is an indication to prove the uniqueness of the data source.

The API is introduced

UseMutableSource can use non-traditional data sources. It functions similar to Context API and useSubscription. (For students who have not used useSubscription, it is helpful to learn about it.)

First, take a look at the basic uses of useMutableSource:

const value = useMutableSource(source,getSnapShot,subscribe)
Copy the code

UseMutableSource is an hooks argument that takes three parameters:

  • Source: MutableSource < source >You can think of it as a data source object with memory.
  • GetSnapshot (source: source) => Snapshot: a function that takes the data source as an argument to get snapshot informationselectorFilter the data of external data sources to find the desired data source.
  • subscribe: (source: Source, callback: () => void) => () => voidSource is the first parameter of useMutableSource, and callback is the second parameter of useMutableSource. When the data Source changes, a snapshot is taken to retrieve new data.

UseMutableSource characteristics

UseMutableSource and useSubscription function are similar:

  • Both require memorized ‘configured objects’ that can be evaluated externally.
  • Both require a way to subscribe to and unsubscribe from sourcessubscribe.

Other features of useMutableSource include:

  • UseMutableSource requires the source as an explicit parameter. That is, you need to pass in the data source object as the first parameter.
  • UseMutableSource Data read with getSnapshot is immutable.

UseMutableSource tracks the version number of a MutableSource and then reads data, so if the two are inconsistent, a read exception may occur. UseMutableSource checks the version number:

  • The version number is read the first time the component is mounted.
  • When rerender is used, make sure the version number is the same and then read the data. Otherwise errors will occur.
  • Ensure consistency of data source and version number.

The design specification

When reading an external data source via getSnapshot, the value returned should be immutable.

  • ✅ : getSnapshot: source => array. from(source.friendids)
  • ❌ error: getSnapshot: source => source.friendIDs

The data source must have a global version number that represents the entire data source:

  • ✅ : getVersion: () => source.version
  • ❌ error: getVersion: () => source.user.version

Next, I’ll refer to the github example to explain how to use it:

Example a

Example 1: Route changes in subscription history mode

For example, there is a scenario where the subscription route changes in a non-human situation. Show the corresponding location.pathname and see how it is handled using useMutableSource. In this scenario, the external data source is location information.

// Create an external data source through createMutableSource.
// The data source object is window.
// Use location.href as the version number of the data source.
const locationSource = createMutableSource(
  window.() = > window.location.href
);

// Get snapshot information, this is the location. Pathname field, this can be reused, when the route changes, then the snapshot function will be called, to form a new snapshot information.
const getSnapshot = window= > window.location.pathname

// Subscribe function.
const subscribe = (window, callback) = > {
   // Use popState to listen for route changes in history mode. When the route changes, run the snapshot function to obtain new snapshot information.
  window.addEventListener("popstate", callback);
   // Cancel the listener
  return () = > window.removeEventListener("popstate", callback);
};

function Example() {
  // Pass the data source object, snapshot function, and subscription function through useMutableSource to form a pathName.
  const pathName = useMutableSource(locationSource, getSnapshot, subscribe);

  // ...
}
Copy the code

To describe the process:

  • First of all bycreateMutableSourceCreate a data source object called Window. Use location.href as the version number of the data source. If href changes, the data source has changed.
  • Obtain snapshot information, in this case the location. Pathname field, this can be reused, when the route changes, then the snapshot function will be called, to form a new snapshot information.
  • throughpopstateListening to thehistoryWhen routes change in mode, run the snapshot function to obtain new snapshot information.
  • throughuseMutableSource, pass in the data source object, snapshot function, subscription function, formpathName

While this example 🌰 may not be enough to get you started on useMutableSource, let’s take another example to see how useMutableSource works with Redux.

Example 2

Example 2: in reduxuseMutableSourceuse

Redux can write custom hooks from useMutableSource — useSelector, which reads the state of the data source and re-captures the state when the data source changes, subscribes to updates. Let’s see how useSelector is implemented.

const mutableSource = createMutableSource(
  reduxStore, // Use Redux's store as the data source.
  // State is immutable and can be used as the version number of the data source
  () = > reduxStore.getState()
);

// Save the mutableSource by creating a context.
const MutableSourceContext = createContext(mutableSource);

// Subscription store changes. If store changes, run getSnapshot
const subscribe = (store, callback) = > store.subscribe(callback);

// Custom hooks useSelector can be used inside each connect to get data source objects via useContext.
function useSelector(selector) {
  const mutableSource = useContext(MutableSourceContext);
   // use useCallback to make getSnapshot memory.
  const getSnapshot = useCallback(store= > selector(store.getState()), [
    selector
  ]);
   // Finally, we essentially subscribe to state changes using useMutableSource.
  return useMutableSource(mutableSource, getSnapshot, subscribe);
}
Copy the code

The general process is like this:

  • Use Redux’s Store as the data source objectmutableSource. State is immutable and can be used as the version number of the data source.
  • Save the data source object by creating a contextmutableSource.
  • Declare subscription functions and subscribe store changes. Store changes, executegetSnapshot
  • Custom hooksuseSelectorIt can be used within each connect, fetching data source objects via useContext. withuseCallbackMake getSnapshot memory.
  • The final result is essentially subscribing to external state changes with useMutableSource.

Pay attention to the problem

  • GetSnapshot needs to be memorized when it is created, just as useCallback did with getSnapshot in the above process. If it is not memorized, components will be rendered frequently.
  • In the latest react-Redux source code, the new API has been used to subscribe to external data sources, but notuseMutableSourcebutuseSyncExternalStore, specifically becauseuseMutableSourceThere is no built-in selectorAPI, and you need to re-subscribe to the Store every time the selector changes, or if there is no memorization of the useCallback API, then re-subscribe. Please refer to the detailsUseMutableSource – useSyncExternalStore.

Three practice

I’m going to use an example to put the creation Mutablesource into practice to make the process clearer.

Again, redux and createMutableSource are used to reference external data sources. The 18.0.0-alpha versions of React and react- DOM are used here.

import  React , {
    unstable_useMutableSource as useMutableSource,
    unstable_createMutableSource as createMutableSource
} from 'react'

import { combineReducers , createStore  } from 'redux'

/* number Reducer */
function numberReducer(state=1,action){
    switch (action.type){
      case 'ADD':
        return state + 1
      case 'DEL':
        return state - 1
      default:
        return state
    }
}
/* Register reducer */
const rootReducer = combineReducers({ number:numberReducer  })
/* Synthesizes Store */
const Store = createStore(rootReducer,{ number: 1  })
/* Register external data source */
const dataSource = createMutableSource( Store ,() = > 1 )

/* Subscribe to external data sources */
const subscribe = (dataSource,callback) = >{
    const unSubScribe = dataSource.subscribe(callback)
    return () = > unSubScribe()
}

/ *TODO:A * /
export default function Index(){
    /* Get data snapshot */
     const shotSnop = React.useCallback((data) = >({... data.getState()}),[])/* hooks: use */
    const data = useMutableSource(dataSource,shotSnop,subscribe)
    return <div>
        <p>Embrace React 18 🎉🎉🎉</p>Praise: {data. The number}<br/>
        <button onClick={()= >Store. Dispatch ({type:'ADD'})} > Like</button>
    </div>
}
Copy the code

The first part uses combineReducers and createStore to create redux Store. The focus is on the second part:

  • First create the data source with createMutableSource, Store as the data source,data.getState()As the version number.
  • The second point is snapshot information, which in this case is state in store. So in theshotSnopAgain, get the status via getState, normally shotSnop shouldSelectorI’m mapping all the states here.
  • The third is to passuseMutableSourcePass in the data source, snapshot, and subscription functions, and the resulting data is the external data source referenced.

Let’s take a look at the results:

Four principles analysis

UseMutableSource is already planned for React V18, so the implementation principles and details will have to be adjusted in advance of the release of React V18.

1 createMutableSource

react/src/ReactMutableSource.js -> createMutableSource

function createMutableSource(source,getVersion){
    const mutableSource = {
        _getVersion: getVersion,
        _source: source,
        _workInProgressVersionPrimary: null._workInProgressVersionSecondary: null};return mutableSource
}
Copy the code

The createMutableSource principle is very simple, similar to createContext and createRef. Create a createMutableSource object,

2 useMutableSource

The useMutableSource principle is also less mysterious, as it is up to the developer to inject the external data source into state and then write the subscription function. The idea behind useMutableSource is to take what developers need to do and make it their own 😂😂😂 so developers don’t have to write code for it. Essentially useState + useEffect:

  • UseState is responsible for the update.
  • UseEffect takes care of subscriptions.

And then let’s see how it works.

react-reconciler/src/ReactFiberHooks.new.js -> useMutableSource

function useMutableSource(hook,source,getSnapshot){
    /* Get the version number */
    const getVersion = source._getVersion;
    const version = getVersion(source._source);
    /* Save the current Snapshot with useState to trigger the update. * /
    let [currentSnapshot, setSnapshot] = dispatcher.useState(() = >
       readFromUnsubscribedMutableSource(root, source, getSnapshot),
    );
    dispatcher.useEffect(() = > {
        /* Wrapping function */
        const handleChange = () = > {
            /* Trigger update */
            setSnapshot()
        }
        /* Subscribe to update */
        const unsubscribe = subscribe(source._source, handleChange);
        /* Unsubscribe */
        return unsubscribe;
    },[source, subscribe])
}
Copy the code

The core logic is preserved in the above code:

  • First of all bygetVersionTo get the data source version number, useuseStateSave the current Snapshot. SetSnapshot is used to trigger the update.
  • In useEffect, the subscription is bound to the wrapped handleChange function, which calls the setSnapshot real update component.
  • So useMutableSource is essentially useState.

Five summarizes

Today covers the background, usage, and rationale of useMutableSource. If you want to read React V18, you can clone the new version and try out the new features that will help you understand useMutableSource. The next chapter will continue to focus on React V18.

This article is also synchronized to the booklet, interested students can also read in the booklet system.

Reference documentation

  • useMutableSource RFC