preface

Life is a process of accumulation, you will always fall, even if you fall, you should also know how to grasp a handful of sand in hand. – ding

Every requirement you code, every pothole you step on, every bug you fix, every knowledge you learn, and every article you read adds to your technological castle. Today we will start by implementing the requirement of state sharing between React and Vue apps. We will learn about the features of React and Vue that we rarely use, but which are absolutely necessary when we meet these special requirements ๐Ÿคน๐Ÿป

Needs & Problems

Demand status quo

In my daily business development of Byte, I need to mount different business components on a platform page that does not belong to our takeover. Since each business component has its own mounting location and timing, and can be regarded as a separate React application, we use Webpack for multi-entry packaging. Type multiple React apps and mount the business components on this page by introducing an SDK.

The problem

Multi-entry packaging can result in state being shared within business components, but not between business components. And each component may need the same data internally, resulting in the same network request being sent multiple times on the same page.

So the problem and ultimate goal is to solve the problem of state sharing between multiple React applications:

  • A certain state needs to be mounted differently in multiple pagesDOMSharing between business components of nodes (access + updates)
  • Interactions within one component need to trigger status updates for other components

The solution

Mount state on the global Window objectEventEmitterTriggered update

Using class inheritance EventEmitter stores and shares data by declaring common variables in the class, and implements data sharing and updating by sending event subscriptions. Use singleton synchronization in Windows to allow multiple components to synchronize and share data using the same publish subscribe instance. EventEmitter we directly use the On listening events and emit firing events provided by the Eventemitter3 library. Here is the TS Demo code

import EventEmitter from 'eventemitter3'

// Define the event constants that fire
export const ACTION = {
  ADD_COUNT: 'add-count',}as const

Declare the Store interface
export interface IStore {
  count: {
    value:number.addCount:() = > void}}// Store data by inheriting From EventEmitter's class
export class MyEmitter extends EventEmitter {
  public store: IStore = {
   count: {value:1.addCount:() = >{this.count.value++}
		}
  }
}

// Mount the class instance in the Window and ensure that the same instance is used in different components
export const getMyEmitter: () = > MyEmitter = () = > {
  if (window.myEmitter) {
    return window.myEmitter
  }
  window.myEmitter = new Emitter()
  const currentEmitter = window.myEmitter
  const store = currentEmitter.store
  ee.on(ACTION.ADD_COUNT, store.count.addCount, store.count)
  return window.myEmitter
}

Copy the code

This is a very primitive way of sharing states. Let’s see how React works

import React,{ useState, useEffect} from 'react'
import {getMyEmitter, ACTION} from './getMyEmitter'

/ / use
const emitter = getMyEmitter()
const CountDemo = () = >{
  return <div>{emitter.store.count.value}</div>
}

// Trigger the event
const ButtonDemo = () = >{
  return <button onClick={()= >{emitter.emit(ACTION.ADD_COUNT)}}>add count</button>
}

Copy the code

advantages

This solution is primitive, but it does solve the problem we face:

  • Solve the problem that multi-entry packaged applications cannot use the unified data source, and maintain and manage the data status of multiple applications in a unified manner
  • Single data source

disadvantages

But the disadvantages are also obvious:

  • Data is exposed globallywindowObject, not elegant, unsafe
  • Using event-triggered methods to synchronize data does not seem to be the caseReactThe recommendation
  • Once you have more events to register, it becomes difficult to manage events and state

Two, single entry packaging + portal

React Recommended Practices

In Scenario 1, we said that using events to synchronize data is not recommended. What is recommended for data sharing? Use useContext demo to upgrade the status of each component to its nearest parent node.

// Data to be shared
import ReactDOM from "react-dom";
import React, { createContext, useContext, useReducer } from "react";
import "./styles.css";

const ThemeContext = createContext();
const DEFAULT_STATE = {
  theme: "light"
};

const reducer = (state, actions) = > {
  switch (actions.type) {
    case "theme":
      return { ...state, theme: actions.payload };
    default:
      returnDEFAULT_STATE; }};const ThemeProvider = ({ children }) = > {
  return (
    <ThemeContext.Provider value={useReducer(reducer, DEFAULT_STATE)} >
      {children}
    </ThemeContext.Provider>
  );
};

const ListItem = props= > (
  <li>
    <Button {. props} / >
  </li>
);

const App = props= > {
  const [state] = useContext(ThemeContext);
  const bg = state.theme === "light" ? "#ffffff" : "# 000000";
  return (
    <div
      className="App"
      style={{
        background: bg
      }}
    >
       <ul>
          <ListItem value="light" />
          <ListItem value="dark" />
 	   </ul>
    </div>
  );
};


const Button = ({ value }) = > {
  const [state, dispatcher] =  useContext(ThemeContext);
  const bgColor = state.theme === "light" ? "# 333333" : "#eeeeee";
  const textColor = state.theme === "light" ? "#ffffff" : "# 000000";

  return (
    <button
      style={{
        backgroundColor: bgColor.color: textColor
      }}
      onClick={()= > {
        dispatcher({ type: "theme", payload: value });
      }}
    >
      {value}
    </button>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(
  <ThemeProvider>
    <App />
  </ThemeProvider>,
  rootElement
);

Copy the code

The real problem to solve

If the React recommendation is used to implement data sharing, we need to place all business components under the same React Tree while ensuring that they can still be mounted to different DOM nodes on the page. React events bubble, state is shared, and the React lifecycle works as expected only when all business components are under the same React Tree. So first we need to change from multi-entry packaging to single-entry packaging, at least for single pages. It is very easy to change the mode of multi-entry packaging to single entry packaging, directly change the webPack configuration ok. Then, we solved how to ensure that different business components are mounted on different DOM nodes under the premise of the same React Tree.

Just a quick word about the problem we need to solve now. We all know that mounting a React APP on a DOM node is simply reactdom. render(< APP />, targetElement), but each business component has its own DOM node. If business components execute reactdom.render individually, there is no guarantee that all business components will be under the same React Tree, and React events will not bubble, state will not be shared, and React lifecycle will not work as expected.

So the next problem we need to solve is: how to ensure that different business components can be mounted on different DOM nodes, but they are still under the same React Tree?

Start solving problems

After the main application of reactdom. render, the child components can be mounted at different locations on the page ๐Ÿค”, which reminds me of Modal in Ant-Design, when the user needs to deal with transactions and does not want to jump to the page to interrupt the workflow, You can use Modal to open a floating layer in the middle of the current page to carry the corresponding operation. Modal has a getContainer property in it, which says that the default Modal mount location is document.body, and you can specify the HTML node to be modally mounted, and when the value is false it’s mounted in the current DOM.

That means the Modal component we wrote in the React application was originally mounted in the same location as the main application, but ant-Design made it in document.body by default. Isn’t that the solution we were looking for? What is the source code for Ant-Design?

We first find the Modal component popover in Ant-Design and see that the popover is implemented through the RC-Dialog package.

Then we look for the rC-Dialog implementation, and we find that the RC-Dialog uses a Portal component package layer when mounted.

Let’s go to the RC-util package and see how its Portal component is implemented.

Alas, I said “pa” on Github masturbation, very soon ah! And then it comes up with an Ant-design Modal, an RC-dialog, a re-util, and I found it all. I found it all! Once found, of course, the traditional React API stops there. ReactDOM is on my nose. I’m not looking at the document. I smiled and was ready to shut down Github because by this time, according to the traditional Github point-to-point, I had finally found the answer — reactdom.createPortal.

Reactdom.createportal can place components in any DOM of HTML, so the Portal component behaves like a normal React child because it’s still in the React Tree. This means that features like context, event bubbling, and React lifecycle can still be used.

Reactdom.createporal is simply wrapped and can be used anywhere

interface IWrapPortalProps {
  elementId: string // Create createPortal Container with the ID
  effect: (container: HTMLElement, targetDom: Element) = > void // Get the mount location and insert the container into the target nodetargetDom? : Element }/** ** Use createPortal to mount the same React tree on different DOM *@param {*} IWrapPortalProps
 * @returns* /
export const WrapPortal: React.FC<IWrapPortalProps> = (props) = > {
  const [container] = useState(document.createElement('div'))
  useEffect(() = > {
    container.id = props.elementId
    if(! props.targetDom) {return
    }
    props.effect(container, props.targetDom, props.elementId)
    return () = > {
      container.remove()
    }
  }, [container, props])
  return ReactDOM.createPortal(props.children, container)
}

/ / use
const effect = (container: HTMLElement, targetDom: Element) = >{ targetDom! .insertAdjacentElement('afterbegin', container)
}
const targetDom = document.body

<WrapPortal effect={effect} targetDom={targetDom} elementId={'modal-root'} ><button>Modal</button>
</WrapPortal>
Copy the code

portal

Here’s a review of React and Vue Portal and how to use it

Portal components can be placed in any DOM of THE HTML. The Portal component behaves the same as normal React and Vue child nodes because it is still in the React and Vue Tree and does not matter where it is in the DOM Tree. This means features like context, event bubbling, and React and Vue life cycles can still be used.

  • Event bubbling works properly— by spreading the event toReactTree ancestor nodes, event bubbling will work as expected while withDOMIn thePortalNode positions are independent.
  • React and Vue control Portal nodes and their lifecycle-react and Vue still control their lifecycle-life while rendering child elements through Portal.
  • Portal only affects the DOM structurePortalOnly affectsHTML DOMStructure and does not affectThe React, VueThe component tree.
  • Predefined HTML mount points– the use ofPortalYou need to define an HTML DOM element asPortalMount points of components.

React, Vue Portal can be useful when we need to render subcomponents outside the normal DOM hierarchy without breaking default behavior such as event propagation through the React component tree hierarchy:

  • Modal dialog box
  • The tool tip
  • Suspended card
  • Load prompt component
  • inShawdow DOMIn the mountThe React, Vuecomponent

Vue 3.0 added the concept of Teleport, which was not supported in Vue 2.

const app = Vue.createApp({});
app.component('modal-button', {
  template: `  
       
        
       `.data() {
    return { 
      modalOpen: false
    }
  }
})
app.mount('#app')
Copy the code

Vue2 does not have portal concept, is it not supported? We can use this open source project portal-Vue from 3K Star

<template>
  <div>
    <button @click="disabled = ! disabled">Toggle "Disable"</button>
    <Design-Container>
      <Design-Panel color="green" text="Source">
        <p>
          The content below this paragraph is
          rendered in the right/bottom (red) container by PortalVue
          if the portal is enabled. Otherwise, it's shown here in place.
        </p>
        <Portal to="right-disable" :disabled="disabled">
          <p class="red">This is content from the left/top container (green).</p>
        </Portal>
      </Design-Panel>
      <Design-Panel color="red" text="Target" left>
        <PortalTarget name="right-disable"></PortalTarget>
      </Design-Panel>
    </Design-Container>
  </div>
</template>
<script>
export default {
  data: () = > ({
    disabled: false,}})</script>
Copy the code

conclusion

  • Before: We provided multiple business components to a page of the host platform and packaged them into multiple chunks for the host to use according to the multi-entry packaging method.

  • Problem: The multi-entry approach is very unfriendly to data sharing, solvable but not elegant, which is solution 1 in this paper.

  • Redux, Mobx, unstate, React Context, etc. However, the formal way is to work in the same React App. Since multiple portals are packaged into multiple React apps, we first switch to single portal packaging for single pages to ensure that multiple business components are in the same React App. At the same time, according to the requirement that each business component needs to be mounted in different DOM, we use Portal to wrap a layer for business components to ensure that they are all in the same React Tree.

If you like this article, please click “like”, “Star” and follow me ๐ŸŽฏ

reference

  • Portal: React Portal
  • Vue teleport
  • React createportal