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

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.

Related

How React blocks UI updating in useLayoutEffect

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.

How to change the background of a styled.div if the window is scrolled to the top?

I would like to change the background of a div if the window is scrolled to the top. I have figured out how to determine that in React, and when console.logging my isAtTop variable it will change based on the scroll, however the actual div never seems to receive as the styled.div doesn't change colors. I have made a codePen: https://codesandbox.io/s/xenodochial-chebyshev-4poi3?file=/src/App.js
import "./styles.css";
import React from 'react'
import styled from 'styled-components'
export default function App() {
let isAtTop = true;
let bg = `linear-gradient(to right, #797cd2, #393e9e)`;
function handleScroll() {
isAtTop = window.scrollY === 0;
isAtTop
? (bg = `linear-gradient(to right, #797cd2, #393e9e)`)
: (bg = "white");
console.log({ isAtTop });
}
React.useEffect(() => {
console.log("UseEffect Run");
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
const ColorChangeDiv = styled.div`
background: ${bg};
position: sticky;
width: 100%;
top: 0;
height: 100px;
`
return (
<>
<ColorChangeDiv>
{isAtTop.toString()} ? Hello my background should be {bg}
</ColorChangeDiv>
<div style={{height: '500rem'}}></div>
</>
);
}
I changed your code a little, now I will explain what exactly changed.
Made the functions arrow, so it looks cleaner
Wrapped the scroll event handler in useCallback to exclude unnecessary calls to useEffect, since now this function is in useEffect dependencies and it is a good practice to specify all dependencies used in useEffect.
The background color is now stored in the state. You were storing in a variable, the component is not re-rendered on changing the let variable. This means that even changing the value of the variable, the component will not display itself with the new values. We need a rerender. A change in state causes a rerender. This means that if you want to change the component depending on the changed values, then store them in a state to call the rerender :)
I brought the styled component out, it looks cleaner and plus, it makes no sense to keep it inside the APP component, you can pass the values ​​to the styled component in props. Check out the example below, now the background is passed as props
You have long default background values. And it repeats itself. Such long repetitive things are better to be translated into constants with good understandable names. See I brought it up as DEFAULT_BG.
https://codesandbox.io/s/boring-pasteur-18v0r?file=/src/App.js:361-372
import React, { useCallback, useState, useEffect } from "react";
import styled from "styled-components";
const DEFAULT_BG = `linear-gradient(to right, #797cd2, #393e9e)`;
const ColorChangeDiv = styled.div`
background: ${(p) => p.bg};
position: sticky;
width: 100%;
top: 0;
height: 2000px;
`;
const App = () => {
const [bg, setBg] = useState(DEFAULT_BG);
const handleScroll = useCallback(() => {
setBg(window.scrollY === 0 ? DEFAULT_BG : "white");
}, []);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return (
<>
<ColorChangeDiv bg={bg}>
Hello my background should be {bg}
</ColorChangeDiv>
<div style={{ height: "500rem" }}></div>
</>
);
};
export default App;

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:

Cannot use useState within a function because of asynchronous

I am trying to make a like/unlike button and for this purpose, I use a react hook name liked initialised to false.
this hook is used to modify the like button in front and the like event in back-end.
The problem is that setState is an asynchron function and I cannot have the good state of liked to perform my actions.
I already tried with a useEffect but with liked initialised to false, the action when liked === false is performed on loading. I don t want to.
here is my code
import React from 'react'
import styled from 'styled-components'
import HeartIcon from 'client/components/icons/Heart'
import IconButton from 'client/components/IconButton'
const Heart = styled(HeartIcon)`
stroke: ${p => p.theme.primary};
stroke-width: 2;
fill: transparent;
transition: fill 300ms;
display: block;
width: 100%;
height: auto;
&[aria-checked='true'] {
fill: ${p => p.theme.primary};
}
`
export default function LikeButton(props) {
const [liked, setLiked] = React.useState(false)
function onLikeChange() {
setLiked(prevLiked => !prevLiked)
if (liked === true) {
// creation d'un event like
console.log('like')
} else {
console.log('unlike')
// destroy l'event du like existant
}
}
return (
<IconButton onClick={onLikeChange} {...props}>
<Heart aria-checked={liked} />
</IconButton>
)
}
of course I can switch my actions to perform what I want but I prefere to understand what I'm doing because I'm new to react ;)
What is the way ? Thank you
I think what you are trying to accomplish here needs two useEffect hooks. One for getting the value from the backend on initial load and one for updating the value when it changes. For this you should use two useEffect hooks. The difference between these is quite simple. The hook for setting the initial value is for when your component needs to do something after render, and the hook for setting the liked value when it the state changes is only called when liked changes. Therefor you pass an array as an optional second argument to useEffect.
const [liked, setLiked] = useState()
useEffect(() => {
console.log('get the initial value of liked from backend');
setLiked(initialValue)
}
useEffect(() => {
console.log('Do something after liked has changed', liked);
if (liked === true) {
console.log('like')
} else {
console.log('unlike')
}
}, [liked]);
function onLikeChange() {
setLiked(prevLiked => !prevLiked)
}

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