background

Hooks can use useContext for state management as follows: Create a Context object using createContext to consume useContext in child components

export const AppContext = React.createContext();
Copy the code

Context.Provider created using createContext in the parent component. This component allows the consuming component to subscribe to changes in the Context. When a Provider’s value changes, all of its internal consuming components are rerendered

// parent.jsx
const [position, setPosition] = useState({
	left: 0.top: 0});const change = () = > {
	setPosition({
    	left: position.left++,
        top: 0})}return (
	<AppContext.Provider value={position}>
    	<button onClick={change}>change<button>
        <Child />
    </AppContext.Provider>
)
Copy the code

Consume Context value changes in child components

// child.jsx
const store = useContext(AppContext);
return <span>top: {store.top}; random: {Math.random()}</span>
Copy the code

In the above code, the child can consume the values defined in the parent. The problem with this is that in the Child component we rely on the value of store.top, but when the parent component changes the left value, the random value also changes, indicating that the component has been rendered multiple times. So, is there a way that this subcomponent depends on some value in the Context, and if that value changes then the component will be rerendered, and if any other value changes, that doesn’t cause the subcomponent to be rerendered. The general idea is as follows:

const store = useStore(['left']);
return <span>child</span>
Copy the code

Above we only need the value of left in store, and when the value of left changes, the component starts to re-render. Changes to top in store do not result in rerendering of the component

useReact.memo

The memo can be used to check changes to props. If the props are not changed, the component is not rendered, and the Child component can be split into two components

const InnerChild = React.memo(({ top }) = > (
  <span>
    top: {top}; random: {Math.random()}
  </span>
));
function Child() {
  const store = useContext(AppContext);
  return <InnerChild top={store.top} />;
}
Copy the code

Use React. Memo to wrap the component, because the parent only changes the value of left, so top never changes, and the current component is never rerendered

useuseMemoCaching components

You pass in the creation of functions and an array of dependencies as arguments to useMemo, which recalculates memoized values only when a dependency changes. Then you can change the child component above to the following:

// child
 const store = useContext(AppContext);
 return useMemo(() = > <span>random: {Math.random()}</span>, [store.top]);
Copy the code

The useMemo return value will change only when store.top changes. However, there is a drawback to this approach. When a child component needs to maintain a large amount of state, useMemo dependencies need to be written a lot, which can lead to omissions and state updates, and the DOM tree is not updated.

Break upContext

This idea is borrowed from the publish subscriber model, where publishers publish data and only update the components on which they depend. Here’s how to use it

const { Provider, useModal } = createModel((initState) = > {
	const [count, setCount] = React.useState(0);
  	const [value, setValue] = React.useState('value');
  	const inc = function () {
    	setCount(count + 1);
  	};
  	const dec = function () {
    	setCount(count - 1);
  	};
  	return { count, inc, dec, value, setValue };
});
Copy the code

Then use the Provider in the parent component to provide the value

function Parent() {
	return (
    <Provider>
    	<Child />
        <Count />
    </Provider>)}Copy the code

The Provider provides two components that depend on different values in the Provider, as follows:

const Child = () = > {
  const { count, inc, dec } = useModel(['count']);
  return (
    <div>
      {Math.random()}
      <Button onClick={dec}>-</Button>
      <span>{count}</span>
      <Button onClick={inc}>+</Button>
    </div>
  );
};
const Counter = () = > {
  const { value, setValue } = useModel(['value']);
  return (
    <div>
      {Math.random()}
      <input value={value} onChange={e= > setValue(e.target.value)} />
    </div>
  );
};
Copy the code

In the code above, the Count Child only expects the component to re-render when the value changes, while the Child component only expects the component to re-render when the Count value changes. First, let’s talk about the idea of this approach. In a nutshell, it is the publishing subscriber mode. Provider is the publisher, and Child and Count are the subscribers. When the Provider value changes, all subscribers need to be notified to update it. After receiving the update notification, the subscriber determines whether to update according to the previous value.

Implement the publish subscriber pattern

The first step is to implement a simple publish subscriber pattern

class Subs {
	constructor(state) {
		this.state = state;
        this.observers = [];
	}
    add(observer) {
    	this.observers.push(observer)
    }
    notify() {
		this.observers.forEach(observer= > observer())
	}
    delete(observer) {
    	const index = this.observers.findIndex(item= > item === observer);
        if (index > -1) {
			this.observers.splice(index, 1); }}}Copy the code

The add method is used to add subscribers to the subscription list, and notify notifies all subscribers of updates

Provider

The Provider needs to be wrapped so that when the supplied value changes, all subscribers need to be notified to trigger the update

function createModel(model) {
	const Context1 = createContext(null);
    const Context2 = createContext(null);
    const Provider = ({ initState, children }) = > {
		const containerRef = useRef();
        if(! containerRef.current) { containerRef.current =new Subs(initState);
        }
        const sealedInitState = useMemo(() = > initState, []);
    	const state = model(sealedInitState);
        
        useEffect(() = > {
        	containerRef.current.notify();
        })
        return (
        	<Context1 value={state}>
            	<Context2 value={containerRef.current}>
                	{children}
                </Context2>
            </Context1>)}return {
		Provider
	}
}
Copy the code

The code is relatively simple. It creates two contexts. If the sub-component needs to use useModel([‘count’]) in this way, it will actually consume the Context2 value, and if useModel() consumes Context1 directly.

useModel

This function pushes the update function to the Provider’s subscription list when the child component is created.

const useModel = (deps = []) = > {
	const sealedDeps = useMemo(() = > deps, []);

    if (sealedDeps.length === 0) {
      return useContext(Context1);
    }
    
    const container = useContext(Context2);
    const [state, setState] = useState(container.state);
    const prevDepsRef = useRef([]);
    useEffect(() = > {
    	const observer = () = > {
			const prev = prevDepsRef.current;
            const curr = getAttr(container.state, sealedDeps);
            if(! isEuqal(prev, curr)) { setState(container.state) } prev.current = curr; } container.add(observer)return () = > {
			container.delete(observer)
        }
    }, [])
    return state;
}
Copy the code

In simple terms, when useModel is called, the publisher collects the dependency, and when the value is updated, the observer function is triggered. This function compares whether the value has changed before and after the change, and resets the value of state if it has changed. Finally, return the value of state

This is done by loading updated components on demand

react-tracked

A react-Tracked component can also be updated on demand using react-Tracked. For details, see react-Tracked

Welcome you to pay attention to my github, more like ღ(´ · ᴗ · ‘) than heart