The Provider and inject

The Provider uses the React Context mechanism to transfer data between components across hierarchies. It supports both store and non-store data. For example, if you don’t want to layer data across multiple tiers of components, you can use a Provider component to pass data to descendant components.

Inject the provider-passed Store into the descendant component, which is essentially a higher-order component.

Use Provider and Inject

Create the store

// /store/todoStore.js import {observable, configure, action, computed, autorun} from "mobx"; EnforceActions is enabled, indicating that no modification of the state outside the action is allowed. Configure ({enforceActions: "observed"}); Observable Todos = [{id: "0", // Flag whether the task is complete. False, // Define the task name. Title: "Task 1"}, {id: "1", // Mark whether the task is complete. "2", // Mark whether the task has completed finished: false, // Define the task name. @computed get unfinishedCount() { return this.todos.filter(todo => ! todo.finished).length; @action change(todo) {todo.finished =! todo.finished; } } const todoStore = new TodoStore(); Autorun (() = > {the console. The log (" remaining tasks: "+ todoStore. UnfinishedCount +" a "); //sy-log }); export default todoStore;Copy the code

Export the store

In Mobx, we can create multiple stores, and for easy management, we export stores in a single file:

// /store/index.js

import TodoStore from './todoStore';

export const todoStore = TodoStore;
Copy the code

Into the store

Then inject the store in the root component via the Provider component:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
import { Provider } from 'mobx-react';
import {todoStore} from "./store/index";

ReactDOM.render(
  <Provider todoStore={todoStore} omg="omg-omg">
    <App />
  </Provider>,
  document.getElementById("root")
);
Copy the code

Access to the store

In the child component we get the store passed by the Provider by inject.

Inject data into function components:

import React, {Component, Children, useReducer} from "react"; import {observer as observerLite, Observer, useObserver} from "mobx-react-lite"; import {observer, inject, MobXProviderContext} from "mobx-react"; // Inject data into function components const TodoList = inject("todoStore", "omg" )(props => { return ( <div> <h3>TodoList</h3> {props.todoStore.todos.map(todo => ( <Todo key={todo.id} todo={todo} Change ={props.todostore.change} />))} <Observer> {() => (<p> {props. TodoStore. UnfinishedCount}} < / p >) < / Observer > < p > {props. Omg} < / p > < / div >). }); export default TodoList; Const Todo = observer(({Todo, change}, ref) => {console.log("input value", ref.current && ref.current.value); //sy-log return ( <div> <input id={todo.title} type="checkbox" checked={todo.finished} onChange={() => change(todo)} /> <label for={todo.title}>{todo.title}</label> </div> ); }, {forwardRef: true} );Copy the code

Inject data into class components:

import React, {Component, Children, useReducer} from "react"; import {observer as observerLite, Observer, useObserver} from "mobx-react-lite"; import {observer, inject, MobXProviderContext} from "mobx-react"; @inject("todoStore", "omg") @observer class TodoList extends Component { inputRef = React.createRef(); render() { return ( <div> <h3>TodoList</h3> <input type="text" ref={this.inputRef} /> {this.props.todoStore.todos.map(todo => ( <Todo key={todo.id} todo={todo} change={this.props.todoStore.change} Ref = {this. InputRef}} / >)) < p > unfinished task: {this. Props. TodoStore. UnfinishedCount} a < / p > < p > {this. Props. Omg} < / p > < / div >). } } export default TodoList; Const Todo = observer(({Todo, change}, ref) => {console.log("input value", ref.current && ref.current.value); //sy-log return ( <div> <input id={todo.title} type="checkbox" checked={todo.finished} onChange={() => change(todo)} /> <label for={todo.title}>{todo.title}</label> </div> ); }, {forwardRef: true} );Copy the code

In descendant components, besides using Inject to obtain store passed by Provider, Cosumer component and React. UseContext hook can also be used to obtain store passed by Provider.

Get the data using the Cosumer component

Class TodoList extends Component {inputRef = react.createref (); render() { return ( <MobXProviderContext.Consumer> {({todoStore, omg}) => { return ( <Observer> {() => ( <div> <h3>TodoList</h3> <input type="text" ref={this.inputRef} /> {todoStore.todos.map(todo => ( <Todo key={todo.id} todo={todo} change={todoStore.change} ref={this.inputRef} /> ))} < p > unfinished task: {todoStore. UnfinishedCount} < / p > < p > {omg} < / p > < / div >)} < / Observer >); }} </MobXProviderContext.Consumer> ); } } export default TodoList; Todo = observer(React. Callback (ref) => {callback {Todo, change} = props; // console.log("input value", ref.current && ref.current.value); //sy-log return ( <div> <input type="checkbox" checked={todo.finished} onChange={() => change(todo)} /> {todo.title} </div> ); }));Copy the code

Use React. UseContext hook to get data

// Use useContext const TodoList = observer(props => {const {todoStore, omg} = react.usecontext (MobXProviderContext); return ( <div> <h3>TodoList</h3> {todoStore.todos.map(todo => ( <Todo key={todo.id} todo={todo} Change = {todoStore. Change}} / >)) < p > unfinished task: {todoStore. UnfinishedCount} < / p > < p > {omg} < / p > < / div >). }); export default TodoList; Const Todo = observer(({Todo, change}, ref) => {console.log("input value", ref.current && ref.current.value); //sy-log return ( <div> <input id={todo.title} type="checkbox" checked={todo.finished} onChange={() => change(todo)} /> <label for={todo.title}>{todo.title}</label> </div> ); }, {forwardRef: true} );Copy the code

Provider and Inject source code analysis

Let’s first look at the source of the Provider:

Provider source code Analysis

import React from "react" import { shallowEqual } from "./utils/utils" import { IValueMap } from "./types/IValueMap" // Export const MobXProviderContext = React. CreateContext <IValueMap>({}) export interface ProviderProps extends IValueMap { children: React.ReactNode } export function Provider(props: ProviderProps) { const { children, ... Const parentValue = React. UseContext (MobXProviderContext) const parentValue = React. // Create a mutable ref object using useRef hook, The initial value of its.current property is the current value of the current Context object and stores const mutableProviderRef = react.useref ({... parentValue, ... Stores}) / / through ref object attributes of the current access to save the current value in the Context of ref object Context object and stores const value = mutableProviderRef. Current the if (__DEV__) { const newValue = { ... value, ... stores } // spread in previous state for the context based stores if (! shallowEqual(value, newValue)) { throw new Error( "MobX Provider: The set of provided stores has changed. See: }} / / https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error. ") will be stored in the variable object Context in the ref The current value of the Context object and the value property that stores pass to the Context Context object // When the value value changes, The Provider within the child components will render the return < MobXProviderContext. The Provider value = {value} > {children} < / MobXProviderContext Provider >} Provider.displayName = "MobXProvider"Copy the code

In Provider source code:

  1. Use useContext hook to create a React Context object, so that the Provider can get the current value of the Context object and the Provider React Context.

  2. We then create a function component called Provider, which retrieves the current value of the Context Context object and stores the current value in a mutable ref object

  3. Finally, the current value of the Context stored in the mutable object REF and stores are passed to the value property of the Context’s Provider React component. When the value property of the Provider changes, Child components within the Provider are also re-rendered

Inject source code analysis

import React from "react" import { observer } from "./observer" import { copyStaticProperties } from "./utils/utils" import { MobXProviderContext } from "./Provider" import { IReactComponent } from "./types/IReactComponent" import { IValueMap } from "./types/IValueMap" import { IWrappedComponent } from "./types/IWrappedComponent" import { IStoresToProps } from "./types/IStoresToProps" /** * Store Injection */ function createStoreInjector( grabStoresFn: IStoresToProps, component: IReactComponent<any>, injectNames: string, makeReactive: boolean ): IReactComponent<any> { // Support forward refs let Injector: IReactComponent<any> = React.forwardRef((props, ref) => { const newProps = { ... } const context = React. UseContext (MobXProviderContext) // Perform user passed back to the new props function Object. The assign (newProps, grabStoresFn (context | | {}, NewProps) | | {}) / / add the ref object to the new component on the props of the if (ref) {newProps. Ref = ref} / / according to the incoming components and props to create a new component to return React.createElement(component, newProps)}) // Inject when the first argument is function, Injector = Observer (Injector) Injector["isMobxInjector"] = true // Assign late to  suppress observer warning // Static fields from component should be visible on the generated Injector copyStaticProperties(component, Injector) Injector["wrappedComponent"] = component // added the displayName injector.displayname = for the Injector component getInjectName(component, injectNames) return Injector } function getInjectName(component: IReactComponent<any>, injectNames: string): string { let displayName const componentName = component.displayName || component.name || (component.constructor && component.constructor.name) || "Component" if (injectNames) displayName = "inject-with-" + injectNames + "(" + componentName + ")" else displayName = "inject(" + componentName + ")" return displayName } function grabStoresByName( storeNames: Array<string> ): (baseStores: IValueMap, nextProps: React.Props<any>) => React.PropsWithRef<any> | undefined { return function(baseStores, nextProps) { storeNames.forEach(function(storeName) { if ( storeName in nextProps // prefer props over stores ) return if (! (storeName in baseStores)) throw new Error( "MobX injector: Store '" + storeName + "' is not available! Make sure it is provided by some Provider" ) nextProps[storeName] = baseStores[storeName] }) return nextProps } } /** * Provide multiple function types for inject function definitions Implement function overloading */ // Export function inject(... stores: Array<string> ): <T extends IReactComponent<any>>( target: T ) => T & (T extends IReactComponent<infer P> ? IWrappedComponent<P> : never) export function inject<S, P, I, C>(fn: fn) IStoresToProps<S, P, I, C> ): <T extends IReactComponent>(target: T) => T & IWrappedComponent<P> /** * higher order component that injects stores to a child. * takes either a varargs list of strings, which are stores read from the context, * or a function that manually maps the available stores from the context to props: * storesToProps(mobxStores, props, context) => newProps */ / the inject argument can be a function that returns newProps, Export function inject(/* fn(stores, nextProps) or... storeNames */ ... storeNames: Array<any>) {if (typeof arguments[0] === "function") {// Inject the first argument is a function let grabStoresFn = arguments[0] return (componentClass: React.ComponentClass<any, any>) => createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, Return (componentClass: componentClass) {return (componentClass: componentClass); React.ComponentClass<any, any>) => createStoreInjector( grabStoresByName(storeNames), componentClass, storeNames.join("-"), false ) } }Copy the code

First look at the inject source body function:

/** * Export function inject(...) export function inject(... stores: Array<string> ): <T extends IReactComponent<any>>( target: T ) => T & (T extends IReactComponent<infer P> ? IWrappedComponent<P> : never) export function inject<S, P, I, C>(fn: fn) IStoresToProps<S, P, I, C> ): <T extends IReactComponent>(target: T) => T & IWrappedComponent<P> export function inject(/* fn(stores, nextProps) or ... storeNames */ ... storeNames: Array<any>) {if (typeof arguments[0] === "function") {// Inject the first argument is a function let grabStoresFn = arguments[0] return (componentClass: React.ComponentClass<any, any>) => createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, Return (componentClass: componentClass) {return (componentClass: componentClass); React.ComponentClass<any, any>) => createStoreInjector( grabStoresByName(storeNames), componentClass, storeNames.join("-"), false ) } }Copy the code

Source code:

  1. Before implementing Inject, we first provide two function type definitions to function overload the Inject function. The first definition is the function type definition when the parameter of inject is store, and the second definition is the function type definition when the parameter of inject is Store name. Therefore, function and Store name are supported as parameters of inject.

  2. CreateStoreInjector creates a responsive component by calling the createStoreInjector method when the injector argument is function. The createStoreInjector method parameters are the function and Component passed in by the user, respectively.

  3. CreateStoreInjector creates a new component by calling the createStoreInjector method when the inject parameter is store name, but the createStoreInjector parameter is different. The first argument is to call the grabStoresByName function, which returns the new props after the merged Store name. The last parameter of the createStoreInjector passed false to indicate that the currently created new component is not reactive.

Let’s examine the createStoreInject method that users use to create new components:

function createStoreInjector( grabStoresFn: IStoresToProps, component: IReactComponent<any>, injectNames: string, makeReactive: boolean ): IReactComponent<any> { // Support forward refs let Injector: IReactComponent<any> = React.forwardRef((props, ref) => { const newProps = { ... } const context = React. UseContext (MobXProviderContext) // Perform user passed back to the new props function Object. The assign (newProps, grabStoresFn (context | | {}, NewProps) | | {}) / / add the ref object to the new component on the props of the if (ref) {newProps. Ref = ref} / / according to the incoming components and props to create a new component to return React.createElement(component, newProps)}) // Inject when the first argument is function, Injector = Observer (Injector) Injector["isMobxInjector"] = true // Assign late to  suppress observer warning // Static fields from component should be visible on the generated Injector copyStaticProperties(component, Injector) Injector["wrappedComponent"] = component // added the displayName injector.displayname = for the Injector component getInjectName(component, injectNames) return Injector }Copy the code

CreateStoreInjector Used the React API createElement() to create a new React Component. Then use the React. ForwardRef to convert the createElement component into a refs forwarder. The new component is then converted into a reactive component based on the makeReactive parameter. When inject is a function, that is, makeReactive is set to true, the Mox-React Observer is used to convert the new component into a responsive component. When inject is a function, If makeReactive is false, the new component is not a reactive component.

Provider and Inject

Above, we respectively analyze the principle of Provider and Inject. Below, we respectively implement a simple version of Provider and Inject according to their principle.

To realize the Provider

import React, {useRef, useContext} from "react"; Import {MobXProviderContext} from "./MobXProviderContext"; export function Provider({children, ... Const parentValue = useContext(MobXProviderContext); const parentValue = useContext(MobXProviderContext); // Use useRef hook to create a mutable ref object whose.current property starts with the current value of the current Context object and stores const mutableProvdierRef = useRef({... parentValue, ... stores}); / / through ref object attributes of the current access to save the current value in the Context of ref object Context object and stores const value = mutableProvdierRef. Current; // Each Context object returns a Provider React component // passing the current value of the Context object stored in the mutable object ref and the value property that stores pass to the Context Context object // when When value changes, The Provider within the child components will render the return (< MobXProviderContext. The Provider value = {value} > {children} < / MobXProviderContext Provider > ); }Copy the code

Implement inject

import React, {useContext} from "react"; import {MobXProviderContext} from "./MobXProviderContext"; export const inject = (... StoreNames) => Component => {// Create a refs forwardRef component const Injector = react. forwardRef((props, Const Context = useContext(MobXProviderContext); const newProps = { ... props, ... context }; if (ref) { newProps.ref = ref; } // Create a new component based on the components and props passed in. Return React. CreateElement (component, newProps); }); return Injector; };Copy the code

Provider and Inject code details: github.com/moozisheng/…