Why isn't my useState() hook setting a new state inside useEffect()? - reactjs

Trying to detect if a <section> element is focused in viewport, I'm unable console.log a single true statement. I'm implementing a [isFocused, setIsFocused] hook for this.
This is my window:
I needed so when Section 2 is positioned at the top of the window, a single console.log(true) shows up. But this happens:
This is my implementation:
import React, { useEffect, useRef, useState } from "react";
const SectionII = (props) => {
const sectionRef = useRef();
const [isFocused, setIsFocused] = useState(false);
const handleScroll = () => {
const section = sectionRef.current;
const { y } = section.getBoundingClientRect();
if(!isFocused && y <= 0) {
setIsFocused(true);
console.log(isFocused, y);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section id="mentorship" ref={sectionRef} style={{borderTop: "1px solid"}}>
<h1>Section 2</h1>
<button>Set hash</button>
</section>
);
};
export default SectionII;
Why wouldn't my state by updated to true with setIsFocused(true) inside if(!isFocused && y <= 0)?
Thanks so much for the insight. I'm really stuck.

When you're using any state management in react, you need to ensure that the change is set before attempting to access the new state value. For your example, you immediately console.log(isFocused, y) following your setState function (changes will only appear on the next DOM render). Rather, you should use a callback with the set state function, setIsFocused(true, () => console.log(isFocused, y)).

Related

React: Trigger a function when a child asynchronously updates its DOM after rendering

Within ParentComponent, I render a chart (ResponsiveLine). I have a function (calculateHeight) calculating the height of some DOM elements of the chart.
To work fine, my function calculateHeight have to be triggered once the chart ResponsiveLine is rendered.
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
How to trigger my function calculateHeight once the chart ResponsiveLine is done rendering?
Here's a simplified code
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
setMarginBottom(calculateHeight(myref));
});
return (
<div ref={myref}>
<ResponsiveLine marginBottom={marginBottom}/>
</div>)
}
EDIT
I can't edit the child ResponsiveLine, it's from a library
You can use the ResizeObserver API to track changes to the dimensions of the box of the div via its ref (specifically the height, which is the block size dimension for content which is in a language with a horizontal writing system like English). I won't go into the details of how the API works: you can read about it at the MDN link above.
The ResponsiveLine aspect of your question doesn't seem relevant except that it's a component you don't control and might change its state asynchronously. In the code snippet demonstration below, I've created a Child component that changes its height after 2 seconds to simulate the same idea.
Code in the TypeScript playground
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.5/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom/client';
// import {useEffect, useRef, useState, type ReactElement} from 'react';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {useEffect, useRef, useState} = React;
// You didn't show this function, so I don't know what it does.
// Here's something in place of it:
function calculateHeight (element: Element): number {
return element.getBoundingClientRect().height;
}
function Child (): ReactElement {
const [style, setStyle] = useState<React.CSSProperties>({
border: '1px solid blue',
height: 50,
});
useEffect(() => {
// Change the height of the child element after 2 seconds
setTimeout(() => setStyle(style => ({...style, height: 150})), 2e3);
}, []);
return (<div {...{style}}>Child</div>);
}
function Parent (): ReactElement {
const ref = useRef<HTMLDivElement>(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
if (!ref.current) return;
let lastBlockSize = 0;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!(entry.borderBoxSize && entry.borderBoxSize.length > 0)) continue;
// #ts-expect-error
const [{blockSize}] = entry.borderBoxSize;
if (blockSize === lastBlockSize) continue;
setMarginBottom(calculateHeight(entry.target));
lastBlockSize = blockSize;
}
});
observer.observe(ref.current, {box: 'border-box'});
return () => observer.disconnect();
}, []);
return (
<div {...{ref}}>
<div>height: {marginBottom}px</div>
<Child />
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!);
reactRoot.render(<Parent />);
</script>
You said,
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
However, parent useEffect does not do that, It fires only after all the children are mounted and their useEffects are fired.
The value of myref is stored in myref.current So your useEffect should be
useEffect(() => {
setMarginBottom(calculateHeight(myref.current));
});
Why don't you send a function to the child component that is called from the useEffect of the child component.
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
someFunction = () => {
setMarginBottom(calculateHeight(myref));
}
return (
<div ref={myref}>
<ResponsiveLine func={someFunction} marginBottom={marginBottom}/>
</div>)
}
// CHILD COMPONENT
const ChildComponent = ({func, marginBotton}) => {
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
func();
}, []);
return <div></div>
}

React Infinite Loading hook, previous trigger

Im trying to make a hook similar to Waypoint.
I simply want to load items and then when the waypoint is out of screen, allow it to load more items if the waypoint is reached.
I can't seem to figure out the logic to have this work properly.
Currently it see the observer state that its on the screen. then it fetches data rapidly.
I think this is because the hook starts at false everytime. Im not sure how to make it true so the data can load. Followed by the opposite when its reached again.
Any ideas.
Here's the hook:
import { useEffect, useState, useRef, RefObject } from 'react';
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
useEffect(() => {
observerRef.current.observe(ref.current);
return () => {
observerRef.current.disconnect();
};
}, [ref]);
return isOnScreen;
}
Here's the use of it:
import React, { useRef } from 'react';
import { WithT } from 'i18next';
import useOnScreen from 'utils/useOnScreen';
interface IInboxListProps extends WithT {
messages: any;
fetchData: () => void;
searchTerm: string;
chatID: string | null;
}
const InboxList: React.FC<IInboxListProps> = ({ messages, fetchData, searchTerm, chatID}) => {
const elementRef = useRef(null);
const isOnScreen = useOnScreen(elementRef);
if (isOnScreen) {
fetchData();
}
const renderItem = () => {
return (
<div className='item unread' key={chatID}>
Item
</div>
);
};
const renderMsgList = ({ messages }) => {
return (
<>
{messages.map(() => {
return renderItem();
})}
</>
);
};
let messagesCopy = [...messages];
//filter results
if (searchTerm !== '') {
messagesCopy = messages.filter(msg => msg.user.toLocaleLowerCase().startsWith(searchTerm.toLocaleLowerCase()));
}
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={elementRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
export default InboxList;
Let's inspect this piece of code
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
We have the following meanings:
.isIntersecting is TRUE --> The element became visible
.isIntersecting is FALSE --> The element disappeared
and
isOnScreen is TRUE --> The element was at least once visible
isOnScreen is FALSE--> The element was never visible
When using a xor (!==) you specify that it:
Was never visible and just became visible
this happens 1 time just after the first intersection
Was visible once and now disappeared
this happens n times each time the element is out of the screen
What you want to do is to get more items each time the element intersects
export default function useOnScreen(ref: RefObject<HTMLElement>, onIntersect: function) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
setIsOnScreen(entry.isIntersecting);
});
}, []);
useEffect(()=?{
if(isOnScreen){
onIntersect();
}
},[isOnScreen,onIntersect])
...
}
and then use it like:
const refetch= useCallback(()=>{
fetchData();
},[fetchData]);
const isOnScreen = useOnScreen(elementRef, refetch);
or simply:
const isOnScreen = useOnScreen(elementRef, fetchData);
If fetchData changes reference for some reason, you might want to use the following instead:
const refetch= useRef(fetchData);
const isOnScreen = useOnScreen(elementRef, refetch);
Remember that useOnScreen has to call it like onIntersect.current()
In InboxList component, what we are saying by this code
if (isOnScreen) {
fetchData();
}
is that, every time InboxList renders, if waypoint is on screen, then initiate the fetch, regardless of whether previous fetch is still in progress.
Note that InboxList could get re-rendered, possibly multiple times, while the fetch is going on, due to many reasons e.g. parent component re-rendering. Every re-rendering will initiate new fetch as long as waypoint is on screen.
To prevent this, we need to keep track of ongoing fetch, something like typical isLoading state variable. Then initiate new fetch only if isLoading === false && isOnScreen.
Alternatively, if it is guaranteed that every fetch will push the waypoint off screen, then we can initiate the fetch only when waypoint is coming on screen, i.e. isOnScreen is changing to true from false :
useEffect(() => {
if (isOnScreen) {
fetchData();
}
}, [isOnScreen]);
However, this will not function correctly if our assumption, that the waypoint goes out of screen on every fetch, does not hold good. This could happen because
pageSize of fetch small and display area can accommodate more
elements
data received from a fetch is getting filtered out due to
client side filtering e.g. searchTerm.
As my assumption. Also you can try this way.
const observeRef = useRef(null);
const [isOnScreen, setIsOnScreen] = useState(false);
const [prevY, setPrevY] = useState(0);
useEffect(()=>{
fetchData();
var option = {
root : null,
rootmargin : "0px",
threshold : 1.0 };
const observer = new IntersectionObserver(
handleObserver(),
option
);
const handleObserver = (entities, observer) => {
const y = observeRef.current.boundingClientRect.y;
if (prevY > y) {
fetchData();
}
setPrevY(y);
}
},[prevY]);
In this case we not focus chat message. we only focus below the chat<div className="item element. when div element trigger by scroll bar the fetchData() calling again and again..
Explain :
In this case we need to use IntersectionObserver for read the element position. we need to pass two parameter for IntersectionObserver.
-first off all in the hanlderObserver you can see boundingClientRect.y. the boundingClientRect method read the element postion. In this case we need only y axis because use y.
when the scrollbar reach div element, y value changed. and then fetchData() is trigger again.
root : This is the root to use for the intersection. rootMargin : Just like a margin property, which is used to provide the margin value to the root either in pixel or in percent (%) . threshold : The number which is used to trigger the callback once the intersection’s area changes to be greater than or equal to the value we have provided in this example .
finally you can add loading status for loading data.
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={observeRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
I hope its correct, i'm not sure. may it's helpful someone. thank you..

Execute Function when a State Variable Changes inside of a useEffect() Hook

so I am trying to create a graph visualization front-end using Antv's G6 and React. I have this useState() variable and function as shown below:
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
The function is in charge of hiding the selected node. However, the problem with running this function as it is, is that it will raise the error TypeError: Cannot read properties of null (reading 'findById') because graph is assigned inside of the useEffect() hook, as shown below:
useEffect(() => {
if (!graph) {
graph = new G6.Graph();
graph.data(data);
graph.render();
hideN();
}
}, []);
It only works as intended if I call the function hideN() inside of the useEffect() hook, otherwise outside of the useEffect() if I console.log(graph) the result would be undefined.
So I wanted to ask, is there a way I could have this function run when the state changes while inside of the useEffect(), or is there a better way to go about this. I'm sorry I am super new to React so still learning the best way to go about doing something. I'd appreciate any help you guys can provide.
Full code:
import G6 from "#antv/g6";
import React, { useEffect, useState, useRef } from "react";
import { data } from "./Data";
import { NodeContextMenu } from "./NodeContextMenu";
const maxWidth = 1300;
const maxHeight = 600;
export default function G1() {
let graph = null;
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graph) {
graph = new G6.Graph(cfg);
graph.data(data);
graph.render();
hideN();
}
}, []);
return (
<div>
<div ref={ref}>
{showNodeContextMenu && (
<NodeContextMenu
x={nodeContextMenuX}
y={nodeContextMenuY}
node={nodeInfo}
setShowNodeContextMenu={setShowNodeContextMenu}
sethideNode={sethideNode}
/>
)}
</div>
</div>
);
}
export { G1 };
Store graph in a React ref so it persists through rerenders. In hideN use an Optional Chaining operator on graphRef.current to call the findById function.
Add hideNode state as a dependency to the useEffect hook and move the hideN call out of the conditional block that is only instantiating a graph value to store in the ref.
const graphRef = useRef(null);
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graphRef.current?.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graphRef.current) {
graphRef.current = new G6.Graph(cfg);
graphRef.current.data(data);
graphRef.current.render();
}
hideN();
}, [hideNode]);

Testing mouse event listener added using ref in React functional component

Hi I have a functional component as shown below:
import React, { useRef, useEffect, useState } from 'react';
const SomeComponent = ({ prop1, ...otherProps}) => {
const divRef = useRef();
useEffect(() => {
divRef.current.addEventListener('mousedown', mouseDownFunc);
}, []);
const mouseDownFunc = () => {
document.addEventListener('mousemove', (el) => {
// call some parent function
});
}
return (
<div
className='test-div'
ref={ divRef }>
</div>
);
};
How do I test a react functional component wherein addEventListener is added using ref inside useEffect which when triggered calls mouseDownFunc.
I'm new to react jest testing, little confused on how to do it.
Testing this sort of component can be tricky, but using #testing-library/react I think I was able to come up with something useful.
I did have to make some changes to your component to expose the API a bit, and I also made some changes so that it stops listening to the events on mouseup which may not be the specific event you want.
Here's the modified component:
// MouseDownExample.js
import React, { useEffect, useState } from "react";
export default ({ onMouseMoveWhileDown }) => {
const [x, setX] = useState(null);
const [listening, setListening] = useState();
// Replaced with mouse move function, should make sure we're unlistening as well
useEffect(() => {
if (listening) {
const onMouseMove = (event) => {
// call some parent function
onMouseMoveWhileDown(event);
console.log(event.clientX);
// purely for testing purposes
setX(event.clientX);
};
const onMouseUp = (event) => {
// stop listening on mouse up
// - you should pick whatever event you want to stop listening
// - this is global so it also stops when the mouse is outside the box
setListening(false);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}
}, [listening, onMouseMoveWhileDown]);
return (
<div
style={{
backgroundColor: "red",
width: 200,
height: 200
}}
className="test-div"
onMouseDown={() => {
// moved this inline, so no ref
setListening(true);
}}
>
X Position: {x}
</div>
);
};
I called out in comments the main differences.
And here's an example test:
// MouseDownExample.test.js
import React from "react";
import { fireEvent, render } from "#testing-library/react";
import MouseDownExample from "./MouseDownExample";
it("shouldn't trigger onMouseMoveWhileDown when mouse isn't down", () => {
const onMouseMoveWhileDown = jest.fn();
const { container } = render(
<MouseDownExample onMouseMoveWhileDown={onMouseMoveWhileDown} />
);
// Note: normally I would use `screen.getByRole` but divs don't have a useful role
const subject = container.firstChild;
fireEvent.mouseMove(
document,
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent
{
clientX: 200
}
);
// hasn't gone down yet
expect(onMouseMoveWhileDown).not.toHaveBeenCalled();
fireEvent.mouseDown(subject);
fireEvent.mouseUp(subject);
// went down then up before moving
fireEvent.mouseMove(document, {
clientX: 200
});
expect(onMouseMoveWhileDown).not.toHaveBeenCalled();
});
it("should trigger onMouseMoveWhileDown when mouse is down", () => {
const onMouseMoveWhileDown = jest.fn();
const { container } = render(
<MouseDownExample onMouseMoveWhileDown={onMouseMoveWhileDown} />
);
// Note: normally I would use `screen.getByRole` but divs don't have a useful role
const subject = container.firstChild;
fireEvent.mouseDown(subject);
fireEvent.mouseMove(document, {
clientX: 200
});
expect(onMouseMoveWhileDown).toHaveBeenCalledWith(
expect.objectContaining({ clientX: 200 })
);
});
What's happening here, is we're rendering the component, then firing events to ensure the onMouseMoveWhileDown function prop is called when we expect.
We have to do expect.objectContaining rather than just the object because it's called with a MouseEvent which contains other properties.
Another test we might want to add is an unmount test to ensure the listeners are no longer triggering events.
You can look at/experiment with this Code Sandbox with this component and the tests. Hope this helps đź‘Ť

setInterval and React hooks produces unexpected results

I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}

Resources