I. Preface
Hello, I'm 👽 , Next, there will be a new series, interpretation of the new features of React v18, which mainly aims at the background, function introduction and principle analysis of the new features, and dares to be the first person to eat crabs. I hope my friends can praise and forward, and then watch a wave official account and share the front-end technology.
The earliest RFC proposal for useMutableSource began in February 2020. It will appear as a new feature in React 18. Summarize useMutableSource with a description in the proposal.
useMutableSource enables React components to safely and effectively 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 should start with state and update. In traditional UI frameworks such as React and Vue, although they all use virtual DOM, they still can not delegate the update unit to the virtual dom. Therefore, the minimum granularity of update is at the component level. The components uniformly manage the data state and participate in scheduling updates.
Back to our protagonist React, since the component controls the state state. In v17 and earlier versions, if React wants to update the view, it can only change the internal data state. Overview of several update methods of React, all of which are inseparable from their own state. Let's take a look at several update modes of React.
- The component itself changes state. Function useState | useReducer, class component setState | forceUpdate.
- props change, the update of sub components brought by component update.
- The context is updated, and the component consumes the current context.
Either of the above methods is essentially a change of state.
- props change comes from the state change of the parent component.
- The change of context comes from the change of value in the Provider, and value is generally a state or a derivative of state.
It can be summarized from the above: the relationship between state and view update model = > view. However, the state is limited to the data inside the component. If the state comes from the outside (away from the component level). So how to complete the conversion from external data source to internal state, change the data source and re render the component?
In the normal mode, first map the external Data to the state | props through the selector selector. This is a complete step. Next, you need to subscribe to the changes of external Data sources. If there are changes, you need to force the update of forceUpdate. The following two charts show data injection and data subscription update.
1.jpg
2.jpg
A typical external data source is the store in redux. How does Redux safely turn the state in the store into the state of the component.
Maybe I can use a piece of code to represent the process from state change to view update 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 is a change */ if(ifHasChange(state,value)){ setReduxState(value) } }) },[ store ]) return <div>...</div> }
But the code in the example has no practical significance and is not the source code. I'm here to let you clearly understand the process. redux and react work essentially like this.
- Subscribing to state changes through store.subscribe is much more complex than in the code snippet. Find the state required by the component through the selector. Let me explain the selector here first, because business components often do not need all the state data in the whole store, but only some of the following states. At this time, it is necessary to select "useful" from the state and merge it with props. Careful students should find that, The selector needs to be linked with mapStateToProps, the first parameter of connect in react redux. For details, it doesn't matter, because today's focus is useMutableSource.
In the above case, there is no useMutableSource. Now using useMutableSource does not need to hand over the subscription to update process to the component for processing. As follows:
/* Create store */ const store = createStore(reducer,initState) /* Create external data source */ const externalDataSource = createMutableSource( store ,store.getState() ) /* Subscribe to updates */ const subscribe = (store, callback) => store.subscribe(callback); function App({ selector }){ /* If the state of the subscription changes, the component will be updated */ const state = useMutableSource(externalDataSource,selector,subscribe) }
- Create an external data source through createMutableSource, and use the external data source through useMutableSource. When the external data source changes, the component is automatically rendered.
The above subscription update is implemented through useMutableSource, which reduces the internal component code of the APP, improves the robustness of the code, and reduces the coupling to a certain extent. Next, let's take a comprehensive look at the new features of V18.
II. Function introduction
For the specific function introduction process, please refer to the latest RFC, createMutableSource and useMutableSource. To a certain extent, they are a bit like createContext and useContext. See the name meaning, that is, creation and use. The difference is that context requires the Provider to inject internal state, while today's protagonist is to inject external state. Then we should first look at how to use both.
establish
createMutableSource creates a data source. It has two parameters:
const externalDataSource = createMutableSource( store ,store.getState() )
- The first parameter is the external data source, such as the store in redux,
- The second parameter: a function. The return value of the function is used as the version number of the data source. Note here ⚠️ To maintain the consistency between the data source and the data version number, that is, if the data source changes, the data version number will change, and the immutable principle (non variability) will be followed to a certain extent. It can be understood that the data version number is an indication to prove the uniqueness of the data source.
api introduction
useMutableSource can use non-traditional data sources. Its function is similar to Context API and useSubscription. (students who have not used useSubscription can learn about it.).
Let's take a look at the basic usage of useMutableSource:
const value = useMutableSource(source,getSnapShot,subscribe)
useMutableSource is a hooks, which has three parameters:
- Source: mutablesource < source > can be understood as a data source object with memory.
- getSnapshot: (source: source) = > snapshot: a function. The data source is used as the parameter of the function to obtain snapshot information. It can be understood as a selector to filter the data of external data sources and find the desired data source.
- Subscribe: (source: source, callback: () = > void) = > () = > void: the subscription function has two parameters. Source can be understood as the first parameter of useMutableSource, and callback can be understood as the second parameter of useMutableSource. When the data source changes, execute snapshot to obtain new data.
useMutableSource features
The useMutableSource and useSubscription functions are similar:
- Both require a 'configured object' with memory to take values from the outside.
- Both require a subscription and un subscribe method.
In addition, useMutableSource has some features:
- useMutableSource requires source as an explicit parameter. That is, you need to pass in the data source object as the first parameter.
- The data read by useMutableSource with getSnapshot is immutable.
About MutableSource version number useMutableSource will track the version number of MutableSource and then read the data. Therefore, if the two are inconsistent, reading exceptions may be caused. useMutableSource checks the version number:
- Read the version number when the component is mounted for the first time.
- When rerender the component, ensure that the version number is consistent, and then read the data. Otherwise, an error will occur.
- Ensure the consistency of data source and version number.
design code
When reading the external data source through getSnapshot, the returned value should be immutable.
- ✅ Correct writing: getsnapshot: source = > array. From (source. Friendids)
- ❌ Wrong writing: getsnapshot: source = > source.friendids
The data source must have a global version number, which represents the entire data source:
- ✅ Correct writing: GetVersion: () = > source.version
- ❌ Wrong writing method: GetVersion: () = > source.user.version
Next, referring to the example on github, I'll talk about how to use it:
Example 1
Example 1: routing changes in subscription history mode
For example, one scenario is to subscribe to route changes under non-human circumstances, show the corresponding location.pathname, and see how to use 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. If the href changes, the data source changes. const locationSource = createMutableSource( window, () => window.location.href ); // Obtain the snapshot information. Here, the location.pathname field is obtained, which can be reused. When the route changes, the snapshot function will be called to form new snapshot information. const getSnapshot = window => window.location.pathname // Subscription function. const subscribe = (window, callback) => { //Listen to the route changes in history mode through pop state. When the route changes, execute the snapshot function to get new snapshot information. window.addEventListener("popstate", callback); //Cancel listening return () => window.removeEventListener("popstate", callback); }; function Example() { // Pass in the data source object, snapshot function and subscription function through useMutableSource to form pathName. const pathName = useMutableSource(locationSource, getSnapshot, subscribe); // ... }
To describe the process:
- First, create a data source object through createMutableSource. The data source object is window. Use location.href as the version number of the data source. If the href changes, the data source changes.
- Obtain the snapshot information. Here, the location.pathname field is obtained, which can be reused. When the route changes, the snapshot function will be called to form new snapshot information.
- Listen to the route changes in history mode through pop state. When the route changes, execute the snapshot function to get new snapshot information.
- Pass in the data source object, snapshot function and subscription function through useMutableSource to form pathName.
Maybe this example 🌰, It's not enough to let you know the function of useMutableSource. Let's give another example to see how useMutableSource works with redux.
Example 2
Example 2: useMutableSource in redux
redux can write user-defined hooks - useSelector through useMutableSource. useSelector can read the state of the data source. When the data source changes, it will re execute the snapshot to obtain the state, so as to subscribe and update. Let's see how useSelector is implemented.
const mutableSource = createMutableSource( reduxStore, // Use the store of redux as the data source. // state is immutable and can be used as the version number of the data source () => reduxStore.getState() ); // Save the data source mutableSource by creating a context. const MutableSourceContext = createContext(mutableSource); // Subscription store changes. Store changes, execute getSnapshot const subscribe = (store, callback) => store.subscribe(callback); // Custom hooks useSelector can be used inside each connect to obtain data source objects through useContext. function useSelector(selector) { const mutableSource = useContext(MutableSourceContext); // Use useCallback to make getSnapshot memorable. const getSnapshot = useCallback(store => selector(store.getState()), [ selector ]); // Finally, essentially, useMutableSource is used to subscribe to state changes. return useMutableSource(mutableSource, getSnapshot, subscribe); }
The general process is as follows:
- Use the store of redux as the data source object mutableSource. state is immutable and can be used as the version number of the data source.
- Save the data source object mutableSource by creating a context.
- Declare the subscription function and subscribe to store changes. When the store changes, execute getSnapshot.
- Custom hooks useSelector can be used inside each connect to obtain data source objects through useContext. Use useCallback to make getSnapshot memorable.
- Finally, essentially, useMutableSource is used to subscribe to external state changes.
Attention problem
- When creating a getSnapshot, you need to memorize the getSnapshot, just like the useCallback processing getSnapshot in the above process. If you don't memorize the getSnapshot, the component will render frequently.
- In the latest react Redux source code, a new api has been used to subscribe to external data sources, but not useMutableSource, but useSyncExternalStore. Specifically, because useMutableSource does not provide a built-in selector api, you need to re subscribe to the store every time the selector changes. If there is no memory processing of APIs such as useCallback, you will re subscribe. Please refer to useMutableSource → useSyncExternalStore for details.
Three practice
Next, I'll use an example to practice createMutableSource to make the process clearer.
Here, redux and createMutableSource are used to reference external data sources. The 18.0.0-alpha versions of react and react DOM are used here.
3.jpg
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 }) /* Synthetic Store */ const Store = createStore(rootReducer,{ number: 1 }) /* Register external data sources */ const dataSource = createMutableSource( Store ,() => 1 ) /* Subscribe to external data sources */ const subscribe = (dataSource,callback)=>{ const unSubScribe = dataSource.subscribe(callback) return () => unSubScribe() } /* TODO: Situation 1 */ 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> fabulous:{data.number} <br/> <button onClick={()=>Store.dispatch({ type:'ADD' })} >give the thumbs-up</button> </div> }
The first part is the process of creating a redux Store with combineReducers and createStore. The focus is on the second part:
- First, create a data source through createMutableSource. Store is the data source and data.getState() is the version number.
- The second point is the snapshot information. The snapshot here is the state in the store. Therefore, in shotsnoop, the state is still obtained through getState. Normally, shotsnoop should be used as a Selector, and all States are mapped here.
- The third is to pass in the data source, snapshot and subscription function through useMutableSource, and the obtained data is the referenced external data source.
Next, let's look at the effect:
4.gif
Four principles analysis
useMutableSource is already planned in React v18, so its implementation principle and details can be adjusted before V18 is officially launched,
1 createMutableSource
react/src/ReactMutableSource.js -> createMutableSource
function createMutableSource(source,getVersion){ const mutableSource = { _getVersion: getVersion, _source: source, _workInProgressVersionPrimary: null, _workInProgressVersionSecondary: null, }; return mutableSource }
The principle of createMutableSource is very simple. Similar to createContext and createRef, it is to create a createMutableSource object,
2 useMutableSource
The principle of useMutableSource is not so mysterious. It turns out that the developer injects the external data source into the state and then writes the subscription function. The principle of useMutableSource is to do what developers should do by themselves 😂😂😂, This saves developers from writing relevant code. Essentially useState + useEffect:
- useState is responsible for updating.
- useEffect is responsible for subscription.
Then let's look at the principle.
react-reconciler/src/ReactFiberHooks.new.js -> useMutableSource
function useMutableSource(hook,source,getSnapshot){ /* Get 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(() => { /* Packing function */ const handleChange = () => { /* Trigger update */ setSnapshot() } /* Subscribe to updates */ const unsubscribe = subscribe(source._source, handleChange); /* Unsubscribe */ return unsubscribe; },[source, subscribe]) }
The core logic is retained in the above code:
- First, get the data source version number through getVersion, save the current Snapshot with useState, and setSnapshot is used to trigger the update.
- In useEffect, the subscription is bound to the wrapped handleChange function, which calls the real update component of setSnapshot.
- So useMutableSource is essentially useState.
V. summary
Today I talked about the background, usage and principle of useMutableSource. Students who want to read can clone the new version of React v18 and try new features, which will be very helpful to understand useMutableSource. In the next chapter, we will continue to focus on React v18.
Reference documents
- useMutableSource RFC