Handle heavy resource/DOM on React Portal - reactjs

I have created a react portal inside my application to handle the use of Modal. The portal target is outside of my React root div as sibling of my root element.
<html>
<body>
<div id="root">{{app.html}}</div>
<div id="modal-root">
<div class="modal" tabIndex="-1" id="modal-inner-root" role="dialog">
</div>
</div>
</body>
</html>
So my Portal contents renders outside of the react application and its working fine. Here is my react portal code
const PortalRawModal = (props) => {
const [ display, setDisplay ] = useState(document.getElementById("modal-inner-root").style.display)
const div = useRef(document.createElement('div'))
useEffect(()=> {
const modalInnerRoot = document.getElementById("modal-inner-root")
if(validate(props.showModalId)) {
if( props.showModalId == props.modalId && _.size(props.children) > 0 ) {
setDisplay("block");
if(_.size(modalInnerRoot.childNodes) > 0) {
modalInnerRoot.replaceChild(div.current,modalInnerRoot.childNodes[0]);
} else {
modalInnerRoot.appendChild(div.current);
}
div.current.className = props.modalInner;
document.getElementById("modal-root").className = props.modalClassName;
document.body.className = "modal-open";
} else {
document.getElementById("modal-root").className = props.modalClassName;
if(div.current.parentNode == modalInnerRoot) {
modalInnerRoot.removeChild(div.current);
div.current.className = "";
}
}
} else {
setDisplay("none");
document.getElementById("modal-root").className = "";
if(div.current.parentNode == modalInnerRoot) {
modalInnerRoot.removeChild(div.current).className = "";
}
document.body.className = "";
}
},[ props.showModalId ])
useEffect(()=> {
document.body.className = display == "none" ? "" : "modal-open";
document.getElementById("modal-inner-root").style.display = display;
return () => {
if(!validate(props.showModalId)) {
document.body.className = "";
document.getElementById("modal-inner-root").style.display = "none"
}
};
},[ display])
useEffect(()=> {
if(_.size(props.children) <= 0){
modalInnerRoot.removeChild(div.current)
document.body.className = "";
document.getElementById("modal-inner-root").style.display = "none";
}
return () => {
if(_.size(props.children) <= 0){
modalInnerRoot.removeChild(div.current)
document.body.className = "";
document.getElementById("modal-inner-root").style.display = "none";
}
}
},[props.children, props.showModalId])
return ReactDOM.createPortal(props.children ,div.current);
}
Whenever the children are passed and modal is mounted, The heavy DOM is painted with little delay. But the same markup takes time, or even crashes the browser tab. Where am I going wrong in handling the heavy DOM operations? Or is there any async way to handle the heavy DOM operations that wont effect the overall performance?

Couple of reasons can attribute for this :
The last effect will always run for every re-render as props.children is an object and hence even if same children was passed again, it'll be a new object.
Direct DOM manipulation is an anti-pattern, as React maintains several DOM references in memory for fast diffing, hence direct mutation may result in some perf hit.Try writing the same in a declarative fashion.
Extract out the portal content into another sub-component and avoid DOM manipulations wherever possible.
One place would be :
if (_.size(props.children) <= 0) {
modalInnerRoot.removeChild(div.current);
}
can be replaced within the render function like :
{React.Children.count(props.children) ? <div /> : null}

You just have to use the modal root as the createPortal host div (the second argument). React will just render there instead of in the regular element.
Then if you need to "manipulate" the HTML, just use plain React. It does not work any differently inside of portaled elements. All createPortal does is tell React to take this part of the tree and attach it under specified element.
Just include a permanent empty div with no style (no need to use any display rules), and it will just receive all HTML you want to render. Then you make the content inside the modal root fixed, but not the modal root itself.
Don't do this:
const div = useRef(document.createElement('div'))
// all kinds of manipulation of this div
return ReactDOM.createPortal(props.children ,div.current);
Do this instead:
const modalRoot = document.getElementById('modal-root');
function ModalWrapper({children}) {
return <div class="modal" id="modal-inner-root" role="dialog">
{ children }
</div>
}
function PortalModal({children}) {
return React.createPortal(
<ModalWrapper>{ children }</ModalWrapper>,
modalRoot
}
function App() {
const [hasConfirmed, setHasConfirmed] = useState(false);
return <div>
// ...
{ !hasConfirmed && <PortalModal>
<h1> Please confirm </h1>
<button onClick={() => setHasConfirmed(true)}>
Yes
</button>
</PortalModal> }
</div>
}
You can perfectly manage the state of the component in the modal, and whether it should show the modal at all. You won't need to do any manual DOM manipulation anymore.
Once you stop rendering the portal, React removes it for you.
Why does the question's code have performance issues?
It does many DOM operations which, among other things, will result in style recalculations. You shouldn't have to do manual DOM operations at all, it's exactly what React is built to handle for you. And it's reasonably efficient at it.
Since you're doing it in useEffect, React has already triggered style recalculations and the result of that was painted to the screen. This is now immediately invalidated, and the browser needs to recalculate some amount of elements.
Which amount of elements?... All of them, because a style recalculation on the body is triggered by changing its classname.
document.body.className = "modal-open";
If you have a heavy DOM, a full style recalculation can quickly take long and cause noticeable stutter. You can avoid this by not touching the body and just adding an overlay div you can show and hide.
Can it cause a tab to crash though? Maybe in extreme cases, but probably not.
It's more likely that you're calling this component in a way that creates an infinite loop. Or you may be ending up doing a ridiculous amount of DOM operations. It's impossible to tell without the full code used when the performance issues were noted.

Related

Keen Slider React Component Always One Step Behind

I installed the keen-slider library in my React project, and used the code from the App.js file in this example to set up a slider with page dots and navigation arrows. I am trying to modify that code, by passing in an array of React components, the size of which can be changed when the user selects or deselects options.
The problem is, the slider's dot count and arrow configuration always lags one step behind. If I move from 1 (default) to 2 pages selected, the rendered dot count stays at 1. When I increase to 3, it moves to 2. If I then decrease to 2, it goes to 3. It only catches up if I interact with the slider. In my App component's return, I place the slider as {keenSlider(outputComponentArray)}. To get outputComponentArray, I have some divs with onClick functions that toggle each page type's selected state. This array:
var selectedResultsConfig = [
['Proposal', outputs.proposal, resultSelectorProposal, setResultSelectorProposal],
['Map', outputs.map, resultSelectorMap, setResultSelectorMap],
['Front Page', outputs.frontPage, resultSelectorFrontPage, setResultSelectorFrontPage],
['Collage', outputs.collage, resultSelectorCollage, setResultSelectorCollage],
['Price Letter', outputs.priceLetter, resultSelectorPriceLetter, setResultSelectorPriceLetter],
['Line Items', outputs.lineItems, resultSelectorLineItems, setResultSelectorLineItems]
]
establishes what name, page component (in the 'outputs' object), and toggle state/setting function correspond to each other, then these buttons are rendered with .map on this array, like so:
{selectedResultsConfig.map((item, index) => {
return <>
{(index === 0) ? null : <> </>}
<div className={item[2] ? 'resultSelectorButton selectedButton' : 'resultSelectorButton'} onClick={() => { resultSelectToggle(item[0]) }}>
<Icon path={item[2] ? mdiCheckboxMarked : mdiCheckboxBlankOutline} size={1} color='#ecd670' />
<h2>{item[0]}</h2>
</div>
</>
})}
and their onClick function does the toggling like this:
function resultSelectToggle(button) {
if (screen === 'proposals') {
for (let i = 0; i < selectedResultsConfig.length; i++) {
if (button === selectedResultsConfig[i][0]) {
selectedResultsConfig[i][3](!selectedResultsConfig[i][2])
}
}
}
}
and then I have a useEffect hook that goes off after those toggles and sets up the final component array, which is fed to keen-slider:
//after the result selector button is toggled
useEffect(() => {
var tempComponentArray = [];
if (screen === 'proposals') {
for (let i = 0; i < selectedResultsConfig.length; i++) {
if (selectedResultsConfig[i][2]) {
tempComponentArray.push(selectedResultsConfig[i][1])
}
}
}
setOutputComponentArray(tempComponentArray);
}, [resultSelectorProposal, resultSelectorMap, resultSelectorFrontPage, resultSelectorCollage, resultSelectorPriceLetter, screen])
I'm not the most experienced with React, and I already know there are better ways of doing some of this, but it's not clear to me what is causing my issue. I was having a similar issue once that was fixed with useEffect, but I've already implemented that here. Any help would be appreciated, thanks.
I have made significant modifications to my original code to simplify it, and I originally passed keenSlider a functional component, but still the problem persists.

Reactjs how to lazy load image after DOM is loaded

I want to make a reactjs page lazy loading images. Therefore I call lazyLoadImages() inside the componentDidMount(). I found that the browser loading indicator located at browser tab still keep spinning all the way until all images are loaded. This make me think what I've done does not provide a true lazy load image experience.
The interesting thing is that if I wrap lazyLoadImages() in setTimeout with a not too short timeout argument, such as 100ms (not 0 or even 50ms), then the page loading indicator will very soon stop spinning and disappear, which gives me a feeling that the page DOM is complete while those images are started to load at a background process.
I thought the componentDidMount and window onload are something similar, but they are not. I can have the same experience as using setTimeout by using the window onload event. But since my application is a SPA, therefore onload event is not suitable. Because onload event only work when I explicitly refresh this page, but not navigating between pages using react-router.
Anyone has idea on this phenomenon and how can I achieve the same without using setTimeout function?
componentDidMount() {
setTimeout(this.lazyLoadCarouselImage, 100);
// or invoke directly -> this.lazyLoadCarouselImage();
}
lazyLoadCarouselImage() {
let images = [];
let loadedCounter = 0;
let lazyLoadImageList = ['https://wallpaperaccess.com/full/637960.jpg','https://wallpaperaccess.com/full/637960.jpg']
for (let i=0; i<lazyLoadImageList.length; i++ ) {
images[i] = new Image();
images[i].onload = ()=> {
loadedCounter++;
if (loadedCounter == lazyLoadImageList.length) {
// update state here saying all images are loaded
}
}
images[i].src = lazyLoadImageList[i];
}
}
There are libraries that can handle the lazy loading using scroll and resize event handlers, while others use Intersection Observer API. Check out Lazy-loading images for details.
Nowadays you can lazy load images simply by adding the attribute loading="lazy" to each <img> element. Just keep in mind that the feature is fairly new, so make sure potential users are using an up to date Browser.
Below is a quick example where I create 100 images that are "lazy loaded":
class App extends React.Component {
render() {
let results = [];
for (let i = 0; i < 100; i++) {
results.push(
<img
key={`image${i}`}
src={`https://placehold.it/4${i < 10 ? `0${i}` : i }x4${i < 10 ? `0${i}` : i }`}
alt="placeholder"
width={`4${i < 10 ? `0${i}` : i }`}
height={`4${i < 10 ? `0${i}` : i }`}
loading="lazy"
/>
)
}
return (
<div style={{ width: '400px' }}>
{results}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Note: Click "Run code snippet", then open the "Network" tab inside your Browser's developer tools, begin to scroll and you should notice how the images load as they become close to the viewport.
seems it's working to me
const [isLoaded, setIsLoaded] = useState(false);
const [isPageLoaded, setIsPageLoaded] = useState(false); //this helps with lazy loading
useEffect(() => {
setIsLoaded(true);
}, []);
useEffect(() => {
if (isLoaded) {
setIsPageLoaded(true);
}
}, [isLoaded]);

ReactJS - Async Dynamic Component Loading

Situation
I receive json from a cms that describes the content that needs to display on any given page. This project has 50+ components so rather than require all of them on every page I'd rather cherry pick them as needed.
Question
How can I
Make sure all components are available for import (I assume this requires some webpack trickery)
When converting the json's content node to jsx, making sure that any component described is rendered out.
Current Thoughts
I can loop through the raw jsx and collect all the tags for a given page then attempt a load for each tag via something like
const name = iteration.tagName;
dynCmps[name] = someAsynchronousLoad(path + name);
Then dispatch a redux event when loading is complete to kick off a fresh render of the page.
As for converting raw text content to react js I'm using ReactHtmlParser
best resources so far
Dynamic loading of react components
http://henleyedition.com/implicit-code-splitting-with-react-router-and-webpack/
This had me stumped for a couple of days. After chatting with a colleague about it for some time it was decided that the amount of work it would take to offload the performance hit of loading all the components upfront is not work it for our scenario of 30-50 components.
Lazy loading CAN BE used but I decided against it as the extra 10ms of loading (if that) isn't going to be noticeable at all.
import SomeComponent from "./SomeComponent.js"
const spoofedComponents = {
SomeComponent: <SomeComponent />
}
const replaceFunc = (attribs, children) => {
const keys = Object.keys(spoofedComponents);
for(var i in keys) {
const key = keys[i];
// lower case is important here because react converts everything to lower case during text-to-html conversion - only react components can be camel case whereas html is pascal case.
if(attribs.name === key.toLowerCase()) {
return spoofedComponents[key];
}
}
return <p>unknown component</p>
}
...
//inside render
const raw = "<SomeComponent><SomeComponent />"
// it's VERY important that you do NOT use self-closing tags otherwise your renders will be incomplete.
{parse(raw, {
replace: replaceFunc
})}
In my case I have 30+ components imported and mapped to my spoofedComponents constant. It's a bit of a nuissance but this is necessary as react needs to know everything about a given situation so that the virtual dom can do what it is supposed to - save on display performance. The pros are that now a non-developer (editor) can build a layout using a WYSIWYG and have it display using components that a developer made.
Cheers!
Edit
I'm still stuck on adding customized props & children.
Edit
Basic props are working with
const spoofedComponents = {
SomeComponent: (opts) => {
let s = {};
if(opts.attribs.style)
s = JSON.parse(opts.attribs.style);
if(opts.attribs.classname) {
opts.attribs.className = opts.attribs.classname;
delete opts.attribs.classname;
}
return <APIRequest {...opts.attribs} style={s}>{opts.children[0].data}</APIRequest>
}
}
...
const replaceFunc = (opts) => {
const keys = Object.keys(spoofedComponents);
for(var i in keys) {
const key = keys[i];
if(opts.name === key.toLowerCase()) {
const cmp = spoofedComponents[key](opts);
return cmp;
}
}
return <p>unknown component</p>
}
Now to figure out how to add child components dynamically..
EDIT
This is working well enough that I'm going to leave it as is. Here is the updated replaceFunc
const replaceFunc = (obj) => {
const keys = Object.keys(spoofedComponents);
for(var i in keys) {
const key = keys[i];
if(obj.name === key.toLowerCase()) {
if(obj.attribs.style)
obj.attribs.style = JSON.parse(obj.attribs.style);
if(obj.attribs.classname) {
obj.attribs.className = obj.attribs.classname;
delete obj.attribs.classname;
}
return React.createElement(spoofedComponents[key], obj.attribs, obj.children[0].data)
}
}
return obj; //<p>unknown component</p>
}

Why does Object.keys(this.refs) not return all keys?

Hi,
so I've redacted some sensitive information from the screen shot, but you can see enough to see my problem.
Now, I'm trying to build the UI for a site that gets data from a weather station.
I'm trying to use react-google-maps' InfoBox, which disables mouse events by default.
It seems that to enable mouse events, you must wait until the DOM is loaded, and then add the event handlers.
react-google-maps' InfoBox fires an onDomReady event (perhaps even upon adding more divs) but seems to never fire an onContentChanged event (I've looked in the node_modules code).
The content I'm putting in the InfoBox is basically a div with a string ref for each type of weather data. Sometimes there comes along a new type of weather data so I want to put that in also, and have the ref be available / usable.
However, immediately after the new divs have been added (and the DOM has been updated to show them), when I try to console log the DOM nodes (the refs refer to the nodes because they are divs and not a custom built component) the latest added ones are undefined.
They do become a div (not undefined) a few renders later.
I've contemplated that this may be because
1) the DOM is not being updated before I'm trying to access the refs, but indeed the UI shows the new divs,
2) string refs are deprecated (React 16.5),
but they work for the divs in comonentDidMount and eventually for new divs in componentDidUpdate,
3) executing the code within the return value of render may be run asynchronously with componentDidMount, but I also tried setTimeout with 3000 ms to the same effect,
4) of something to do with enumerable properties, but getOwnProperties behaves the same way.
In the end I decided I'll console log this.refs and Object.keys(this.refs) within the same few lines of code (shown in the screen shot), and you can see that within one console log statement (where Object.keys was used in the previous line) that while this.refs is an object with 8 keys, the two most recently added refs don't appear in Object.keys(this.refs).
This is probably a super complex interaction between react-google-maps' InfoBox, React's refs, and JavaScript's Object.keys, but it seems like it should be simple and confuses me to a loss.
Can anyone shed some light on why this might be happening??
The code looks something alike:
class SensorInfoWindow extends React.Component {
handleIconClick = () => {
// do stuff here
}
componentDidMount() {
this.addClickHandlers();
}
componentDidUpdate() {
this.addClickHandlers();
}
addClickHandlers = () => {
const keys = Object.keys(this.refs);
for(let i=0; i<keys.length; i++) {
const key = keys[i];
let element = this.refs[key];
if (element !== undefined)
element.addEventListener('click', this.handleIconClick);
}
}
render() {
const { thissensor, allsensors } = this.props;
let divsToAddHandlersTo = [];
const sensorkeys = Object.keys(allsensors);
for (let i=0; i<sensorkeys.length; i++) {
divsToAddHandlersTo.push(
<div
ref={'stringref' + i}
/>
{/* children here, using InfoBox */}
</div>
);
}
return (
<div>
{divsToAddHandlersTo}
</div>
);
}
}
This is, in essence, the component.

React: img onLoad and Chicken/Egg problem I cannot escape

I have a React control that renders a bunch of images. My goal is to avoid the flickering that is caused by an unknown time it takes React to load the images (yes, I know about inline image loading, let's pretend it doesn't exist for a moment)
I have an initialized array in my class:
this.loadedImages = [];
For this purpose I use onLoad in this manner:
render () {
let items = this.props.images.map((value, index) => {
let style = {};
if (this.isImageLoaded(index))
style = value.style;
else
style = {visibility: 'hidden'};
return <img
key={ index }
onClick={ this.onClick }
onLoad={ this.onLoad(index) }
style={ style }
src={ value.image }
alt={ value.alt}/>
});
return (
<div>
{items}
</div>
);
}
}
my onLoad and isImageLoaded look like this:
onLoad = (index) => {
if (!this.isImageLoaded(index)) {
this.loadedImages.push(index);
}
};
isImageLoaded = (index) => {
let isloaded = this.loadedImages.includes(index);
if (isloaded)
console.log(index + " is loaded!");
else
console.log(index + " is NOT loaded ");
return isloaded;
};
The issue is that once my page loads, the images switch from a "not loaded" into a "loaded" mode -- BUT there is only ONE RENDER that occurs before the images are loaded, thus the {visibility: 'hidden'} style remains permanent.
So my page loads without images. Now, if I click my component even once, the images will appear correctly because the component is forced to re-render (since now the images are loaded). BUT there is no option for me to force such a re-draw programmatically from the onLoad function as I'm getting a warning I should not be doing that from render...
My question is: how can I break the chicken/egg problems here and re-render my component once any image completes its loading.
I suggest combining your loadedImages data with the your other image state (as a boolean flag on each) and updating it using setState every time one loads (your headaches are due to this separation and the fact that you are having to manually keep them synchronised).
Then map over the single array of images (including loading state), using something like the src for the key.

Resources