How React blocks UI updating in useLayoutEffect - reactjs

If you open this code in a browser and look in the developer tools under the "elements" tab, you will see that after the button is clicked, the element will be added to the DOM but will not be rendered.
How does React do it? What browser mechanism or API is being used and how?
Also, if you remove "console.log(element)" from useLaoutEffect, then the component will not immediately appear in the "elements" tab in the DOM tree. Why is this happening?
const sleep = (duration) => {
const start = new Date().getTime();
let end = start;
while(end < start + duration) {
end = new Date().getTime();
}
}
const Message = ({boxRef, children}) => {
const msgRef = React.useRef(null);
React.useLayoutEffect(() => {
const element = document.querySelector('.msg');
console.log(element);
const rect = boxRef.current.getBoundingClientRect();
sleep(1000);
msgRef.current.style.top = `${rect.height + rect.top}px`;
}, []);
return <span ref={msgRef} className="msg">{children}</span>;
};
const Index = () => {
const [show, setShow] = React.useState(false);
const boxRef = React.useRef(null);
return (
<div>
<div ref={boxRef} className="box" onClick={() => setShow(prev => !prev)}>Click me</div>
{show && <Message boxRef={boxRef}>Foo bar baz</Message>}
</div>
);
};
ReactDOM.render(<Index />, document.getElementById('root'));
.box {
position: absolute;
width: 100px;
height: 100px;
border: 1px solid green;
user-select: none;
}
.msg {
user-select: none;
position: relative;
border: 1px solid red;
background-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

This was super interesting so I started to look into it. React uses requestAnimationFrame and tries to schedule renders/work between frames. I think it's less React and more that you have overloaded the browser and prevented it from being able to paint.
https://github.com/facebook/react/blob/4189f712c16cfd6365c163525527a30ef2518b99/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L236
You are effectively blocking the browser from painting, despite the DOM update, for 2 seconds.
Paste this into your console to see it happen:
const sleep = (duration) => {
const start = new Date().getTime();
let end = start;
while(end < start + duration) {
end = new Date().getTime();
}
}
requestAnimationFrame(() => sleep(2000)) && document.body.prepend("I appear after browser next paint, which I block")
This is kind of familiar today because Lighthouse/google speed metrics have been targeting time to interactive (TTI) and Long Running Tasks are a big part of the perf game.
You can see if you run the chrome perf profiler during your code, there is a 2 second long running task in red which is preventing paints (well, everything..). Then a quick tiny paint to catch up to the updated DOM.
The DOM Update
Also, if you remove "console.log(element)" from useLaoutEffect, then
the component will not immediately appear in the "elements" tab in the
DOM tree. Why is this happening?
As for this, this is also interesting!
I don't have a definitive answer but I can definitely reproduce it. I would think that just like what React is doing in their render pipeline to prioritize important tasks, the browser is also trying to determine what tasks to execute in what order.
In the case of your querySelector that never gets used (until you log it), this DOM injection gets deprioritized enough to happen after the long running task. We're talking absolutely tiny amounts of time here.
If you interact with it before the blocking code, it gets scheduled earlier.

Related

Updating component on mouseMove event very slow

I'm working on a project where I've got a div that is having it's location update with onMouseMove events of it's parent. The code below shows how I'm doing it:
function Area () {
const [mousePos, setMousePos] = useState({ x: 0, y: 0, })
const handleMouseMove = event => {
setMousePos({
x: event.clientX - event.currentTarget.offsetLeft,
y: event.clientY - event.currentTarget.offsetTop,
})
}
const getStyle = () => { top: mousePos.y, left: mousePos.x, };
return(
<div className='main-area' onMouseMove={handleMouseMove}>
<div style={getStyle()}></div>
</div>
);
}
There are some css rules that help with the positioning etc. but I think they are irrelevant to the question (let me know if not?).
Anyway, my problem is that I'm getting really high CPU usage (~50% with intel i5-3570k) when moving my mouse inside the 'main-area' div, particularly when there are many other children components involved (not sure if this is because of event bubbling or the way the ReactDOM updates?). This is in the development build of react (I've not tried in production build).
Is this way of updating the position just inherently flawed and slow? Or is there a way to modify it slightly to make it faster?
Cheers
I did some refactoring:
function Area () {
const [mousePosX, setMousePosX] = useState(0);
const [mousePosY, setMousePosY] = useState(0);
const handleMouseMove = event => {
setMousePosX(event.clientX - event.currentTarget.offsetLeft)
setMousePosY(event.clientY - event.currentTarget.offsetTop)
}
return(
<div className='main-area' onMouseMove={handleMouseMove}>
<div style={{top: mousePosY, left: mousePosX}}></div>
</div>
);
}
Can you check if the performance is better?

Dynamically increase react-table height to fill screen size

Is this possible to dynamically increase table height, based on the browser window height, when using the virtualized react-table (example that I'm working with) ?
There's lots of empty space in my browser that I wish it'll fill up, yet I don't wish the table to overflow my screen, only to go up to the end of it (with a scrollbar allowing access to the remaining rows, if any).
I tried to use this flex box css code, but it didn't do anything. Note that I've successfully used this css code for a different component and it worked.
.flexbox-container {
display: flex;
height: 100vh;
flex-direction: column;
}
.test1 {
display: flex;
flex-grow: 1;
}
I can see that the react-table example relies on FixedSizeList which seems to fix the height, which perhaps is disabling the flex box? I've tried to remove all the optional props but it didn't work.
<FixedSizeList
height={800}
itemCount={rows.length}
itemSize={35}
width={totalColumnsWidth+scrollBarSize}
>
{RenderRow}
</FixedSizeList>
When my application have multiple datas i solve with pagination
After user have a pagination hell (many-page)
i used react-window to solve problem after i confroted to your probleme
is solved to this code this sell hooks
import React from 'react'
/**
* Get small piece of screen to dynamic height table
*/
const calcHeight = (val) => (val - (Math.round(val / 4)))
export default function useHeight() {
const [height, setHeight] = React.useState(calcHeight(window.innerHeight))
const detectWindowHeight = (h) => setHeight(calcHeight(h))
React.useEffect(() => {
window.addEventListener("resize", (e) => detectWindowHeight(e.target.innerHeight))
return () => window.removeEventListener("resize", (e) => detectWindowHeight(e.target.innerHeight))
}, [])
return { wH: height }
}
you can use variable wH => to dynamic height your table
Thanks to respond me !!!

How to get over Cannot read property 'removeEventListener' of null in react

I have some repeated hover states that runs a function to display some empty or filled icons (like you would see in some e-commerce websites with empty/filled carts). As practice, I wanted to create and put this into a custom hoverHooks component, w/ useRef and useEffect to run some add/remove eventListeners, likeso:
const ref = useRef(null)
function enter() {
setHover(true)
}
function leave() {
setHover(false)
}
useEffect(() => {
ref.current.addEventListener('mouseenter',enter)
ref.current.addEventListener('mouseleave', leave)
return () => {
ref.current.removeEventListener('mouseenter',enter)
ref.current.removeEventListener('mouseleave',leave)
}
})
I did this so that the container holding my icons can just have the ref={ref} without me having to repeatedly write onMouseEnter / onMouseLeave. (I guess my refs are being repeated, but better three letters, and move my hover state to just one place.
Cannot read property 'removeEventListener' of null, is what I get. I read the React 17 docs regarding this, under "potential issues." But their suggestion isn't working (capturing the mutable data by storing it into a variable).
useEffect(() => {
const myRef = ref.current
myRef.current.addEventListener('mouseenter',enter)
myRef.current.addEventListener('mouseleave', leave)
return () => {
myRef.current.removeEventListener('mouseenter',enter)
myRef.current.removeEventListener('mouseleave',leave)
}
})
Any and all advice would be greatly appreciated!
Thank you
Is that what you are looking for?
/* HOVER HOOK */
const useHover = ({ ref, onMouseEnter, onMouseLeave }) => {
React.useEffect(() => {
if (ref.current) {
ref.current.addEventListener('mouseenter',onMouseEnter);
ref.current.addEventListener('mouseleave',onMouseLeave);
}
return () => {
ref.current.removeEventListener('mouseenter',onMouseEnter);
ref.current.removeEventListener('mouseleave',onMouseLeave);
};
},[ref,onMouseEnter,onMouseLeave]);
return;
};
/* APP */
function App() {
const ref = React.useRef(null);
const onMouseEnter = () => console.log("ENTER");
const onMouseLeave = () => console.log("LEAVE");
useHover({ref,onMouseEnter,onMouseLeave});
return(
<div className="app" ref={ref}>
Hover me
</div>
);
}
ReactDOM.render(<App/>, document.getElementById("root"));
.app {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
There is solution from YouTube:
const refHandle = useRef();
<i ref={refHandle}>
// This is to revise the original
refHandle.current.removeEventListener('touchstart', onStartTouchBegin, false);
// To the conditional format,
(ref.current)?.removeEventListener('touchstart', onStartTouchBegin, false);
I came across this recently in a test using jest and #testing-library/react. I was surprised none of automatic cleanup functions were handling this so I did some more digging and found an old git-issue which lead to this post in the react documentation (note, this is in reference to React v17.0 Release Candidate). To quote:
The problem is that someRef.current is mutable, so by the time the cleanup function runs, it may have been set to null. The solution is to capture any mutable values inside the effect:
useEffect(() => {
const instance = someRef.current;
instance.someSetupMethod();
return () => {
instance.someCleanupMethod();
};
});
Again, this was in regards to v17.0 release candidate so I don't suspect this to be a problem in later versions. I am using React v17.0.2 which leads me to believe this problem stems from our code and/or how we are using react-hooks with third-party libs.
I figured I'd share this information anyways in case someone runs across this problem and is looking for a solution that avoids memory leaks from not cleaning stranded event listeners. Cheers
Instead of using ref.current in the cleanup function:
useEffect(() => {
someRef.current.someSetupMethod();
return () => {
someRef.current.someCleanupMethod();
};
});
Capture the ref inside the effect.
useEffect(() => {
const instance = someRef.current;
instance.someSetupMethod();
return () => {
instance.someCleanupMethod();
};
});
Checkout the documentation for more details:
The problem is that someRef.current is mutable, so by the time the
cleanup function runs, it may have been set to null. The solution is
to capture any mutable values inside the effect:

How state of a react hook must update if it has Map?

I have multiple(more than 15) div tags as tiles. I need to emphasis each one if mouse hover on it. So each tag has onMouseEnter/Leave functions as bellow.
<div
key={key}
onMouseEnter={onMouseEnter(key)}
onMouseLeave={onMouseLeave(key)}
>
...
</div>
Also I put each tiles key in a Map data structure.
const onMouseEnter = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.set(key, true)
setIsHover(newIsHover)
}
}
const onMouseLeave = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.delete(key)
setIsHover(newIsHover)
}
}
Since component is hook it put its state in a useState.
const [isHover, setIsHover] = useState(new Map())
What is happening here:
Always I enter a tile: onMouseEnter function called and its key added to map (as expected)
When I leave a tile: always onMouseLeave called but sometimes key is removed (as expected) and tile turned back to its normal shape but sometimes it does not(problem is here, in this situation map updated at setIsHover in onMouseLeave but it does not changed in the component!).
I think map updated as expected but when I move on new tile it does not understand that yet. So it overwrite it with what it has.
PS: example added. Move between tiles with high speed!
Like the class-based components, calls to update state are asynchronous and get queued up. Try using functional state updates to ensure these queued-up updates correctly update the previous state. This should fix race conditions between quick successive setIsHover calls with the same key.
Notice if you move slowly enough between tiles they correctly highlight and unhighlight, but more quickly (like a swipe) and 2 or more can get stuck until you again slowly exit the tile.
const onMouseEnter = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.set(key, true);
return newIsHover;
});
}
}
const onMouseLeave = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.delete(key);
return newIsHover;
});
}
}
But I should note that this is a lot of leg work for simply applying some component styling, especially hovering. It could more simply be achieved using CSS.
tileStyles.css
.tile {
background-color: lightgray;
border: 3px solid black;
height: 100px;
line-height: 100px;
margin: 10px;
text-align: center;
width: 100px;
}
.tile:hover {
border-color: red;
}
tile.jsx
import React from "react";
import { withStyles } from "#material-ui/core";
import "./tileStyles.css";
const styles = {
container: { display: "flex", width: "600px", flexWrap: "wrap" }
};
const Tiles = ({ classes: { container }, tiles }) => {
return (
<div className={container}>
{tiles.map((tl, key) => {
return (
<div className="tile" key={key} name={key}>
hi
</div>
);
})}
</div>
);
};
export default withStyles(styles)(Tiles);
The normal and hovered styles are applied together (at the same time) and CSS/html will manage when it hovered or not. The component no longer requires event listeners and doesn't need to maintain internal state.
Explanation
what means "...calls to update state are asynchronous and get queued up."?
When you call this.setState or a useState update function the update doesn't happen synchronously right then and there, but they are queued up during the current render cycle and batch processed in the order in which they were queued. Perhaps this demo will help illustrate what happens. What confounds this issue is the fact that event processing is also asynchronous, meaning that, when events occur their registered callbacks are placed in the event queue to be processed.

Images Rerendering inside Styled Component when Chrome Dev Tools is open

This is a bit of a strange one and not sure why it's happening exactly.
When the component mounts, I call a function that in my application makes an HTTP request to get an array of Objects. Then I update 3 states within a map method.
enquiries - Which is just the response from the HTTP request
activeProperty - Which defines which object id is current active
channelDetails - parses some of the response data to be used as a prop to pass down to a child component.
const [enquiries, setEnquiries] = useState({ loading: true });
const [activeProperty, setActiveProperty] = useState();
const [channelDetails, setChannelDetails] = useState([]);
const getChannels = async () => {
// In my actual project,this is an http request and I filter responses
const response = await Enquiries;
const channelDetailsCopy = [...channelDetails];
setEnquiries(
response.map((e, i) => {
const { property } = e;
if (property) {
const { id } = property;
let tempActiveProperty;
if (i === 0 && !activeProperty) {
tempActiveProperty = id;
setActiveProperty(tempActiveProperty);
}
}
channelDetailsCopy.push(getChannelDetails(e));
return e;
})
);
setChannelDetails(channelDetailsCopy);
};
useEffect(() => {
getChannels();
}, []);
Then I return a child component ChannelList that uses styled components to add styles to the element and renders child elements.
const ChannelList = ({ children, listHeight }) => {
const ChannelListDiv = styled.div`
height: ${listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
return <ChannelListDiv className={"ChannelList"}>{children}</ChannelListDiv>;
};
Inside ChannelList component I map over the enquiries state and render the ChannelListItem component which has an assigned key on the index of the object within the array, and accepts the channelDetails state and an onClick handler.
return (
<>
{enquiries &&
enquiries.length > 0 &&
!enquiries.loading &&
channelDetails.length > 0 ? (
<ChannelList listHeight={"380px"}>
{enquiries.map((enquiry, i) => {
return (
<ChannelListItem
key={i}
details={channelDetails[i]}
activeProperty={activeProperty}
setActiveProperty={id => setActiveProperty(id)}
/>
);
})}
</ChannelList>
) : (
"loading..."
)}
</>
);
In the ChannelListItem component I render two images from the details prop based on the channelDetails state
const ChannelListItem = ({ details, setActiveProperty, activeProperty }) => {
const handleClick = () => {
setActiveProperty(details.propId);
};
return (
<div onClick={() => handleClick()} className={`ChannelListItem`}>
<div className={"ChannelListItemAvatarHeads"}>
<div
className={
"ChannelListItemAvatarHeads-prop ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.propertyImage})`
}}
/>
<div
className={
"ChannelListItemAvatarHeads-agent ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.receiverLogo})`
}}
/>
</div>
{activeProperty === details.propId ? <div>active</div> : null}
</div>
);
};
Now, the issue comes whenever the chrome dev tools window is open and you click on the different ChannelListItems the images blink/rerender. I had thought that the diff algorithm would have kicked in here and not rerendered the images as they are the same images?
But it seems that styled-components adds a new class every time you click on a ChannelListItem, so it rerenders the image. But ONLY when the develop tools window is open?
Why is this? Is there a way around this?
I can use inline styles instead of styled-components and it works as expected, though I wanted to see if there was a way around this without removing styled-components
I have a CODESANDBOX to check for yourselves
If you re-activate cache in devtool on network tab the issue disappear.
So the question becomes why the browser refetch the image when cache is disabled ;)
It is simply because the dom change so browser re-render it as you mentioned it the class change.
So the class change because the componetn change.
You create a new component at every render.
A simple fix:
import React from "react";
import styled from "styled-components";
const ChannelListDiv = styled.div`
height: ${props => props.listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
const ChannelList = ({ children, listHeight }) => {
return <ChannelListDiv listHeight={listHeight} className={"ChannelList"}>{children}</ChannelListDiv>;
};
export default ChannelList;
I think it has to do with this setting to disable cache (see red marking in image)
Hope this helps.

Resources