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 pages
DOM
Sharing 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 objectEventEmitter
Triggered 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 globally
window
Object, not elegant, unsafe - Using event-triggered methods to synchronize data does not seem to be the case
React
The 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 to
React
Tree ancestor nodes, event bubbling will work as expected while withDOM
In thePortal
Node 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 structure–
Portal
Only affectsHTML DOM
Structure and does not affectThe React, Vue
The component tree. - Predefined HTML mount points– the use of
Portal
You need to define an HTML DOM element asPortal
Mount 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
- in
Shawdow DOM
In the mountThe React, Vue
component
Vue 3.0 added the concept of Teleport, which was not supported in Vue 2.
const app = Vue.createApp({});
app.component('modal-button', {
template: `
I'm a teleported modal! (My parent is "body")
`.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