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

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.

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]);

Handle heavy resource/DOM on React Portal

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.

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.

Background image flashes when changed via state

An example of what I am talking about is here: https://detrum-replication.herokuapp.com/
My problem can be seen when the hovering diamond in the bottom left is clicked. I have both the gradient and background rendered conditionally according to a value in the state. It just cycles from 1-4 and changes the class of the divs according to the number of the state. The only thing that I don't like is the split second flash when the diamond is clicked. Once the images have been loaded and cycled through it no longer flashes so I assume the images have been cached. Is there a way to precache the images I will be using for the background using React? Did I go about this in the wrong way to achieve what I am trying to do? I have tried using a few precachers for React with no luck. Some of my code is as follows:
constructor(props) {
super(props);
this.state = {
bg: 1
}
}
changeSeason = () => {
let current = this.state.bg;
if (current >= 4){
current = 1;
} else {
current++;
}
this.setState(() => ({bg : current}));
localStorage.setItem("bg", current);
};
render() {
return (
<div className={`gradient${this.state.bg}`}>
<div className={`background${this.state.bg}`}></div>
<div className="diamond" onClick={this.changeSeason}><div className="diamond__shadow-bottom"></div><div className="diamond__shadow-right"></div></div>
</div>
)
}
Thanks for taking the time to help me.
You can preload assets on your app when you know for sure you would be needing them. The browser doesn't wait for your javascript/css to parse and know that you need them, thereby preventing the flicker.
You need to add them to your index.html like this
<link rel="preload" href="bg-image.png" as="image">
Look at the MDN docs for reference

Resources