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

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>
}

Related

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

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)).

Avoid runnning an effect hook when Context get updated

I have a component MyContainer which has a state variable (defined via useState hook), defines a context provider to which it passes the state variable as value and contains also 2 children, MySetCtxComponent and MyViewCtxComponent.
MySetCtxComponent can change the value stored in the context invoking a set function which is also passed as part of the context, BUT DOES NOT RENDER it.
MyViewCtxComponent, on the contrary, RENDERS the value stored in the context.
MySetCtxComponent defines also an effect via useEffect hook. This effect is, for instance, used to update the value of the context at a fixed interval of time.
So the code of the 3 components is this
MyContainer
export function MyContainer() {
const [myContextValue, setMyContextValue] = useState<string>(null);
const setCtxVal = (newVal: string) => {
setMyContextValue(newVal);
};
return (
<MyContext.Provider
value={{ value: myContextValue, setMyContextValue: setCtxVal }}
>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
);
}
MySetCtxComponent
(plus a global varibale to make the example simpler)
let counter = 0;
export function MySetCtxComponent() {
const myCtx = useContext(MyContext);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter);
counter++;
}, 3000);
return () => clearInterval(intervalID);
}, [myCtx]);
return <button onClick={() => (counter = 0)}>Reset</button>;
}
MyViewCtxComponent
export function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the contex: {myCtx.value}
</div>
);
}
Now my problem is that, in this way, everytime the context is updated the effect of MySetCtxComponent is run again even if this is not at all required since MySetCtxComponent does not need to render when the context is updated. But, if I remove myCtx from the dependency array of the useEffect hook (which prevents the effect hook when the context get updated), then I get an es-lint warning such as React Hook useEffect has a missing dependency: 'myCtx'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
Finally the question: is this a case where it is safe to ignore the warning or do I have a fundamental design error here and maybe should opt to use a store? Consider that the example may look pretty silly, but it is the most stripped down version of a real scenario.
Here a stackblitz to replicate the case
One pattern for solving this is to split the context in two, providing one context for actions and another for accessing the context value. This allows you to fulfill the expected dependency array of the useEffect correctly, while also not running it unnecessarily when only the context value has changed.
const { useState, createContext, useContext, useEffect, useRef } = React;
const ViewContext = createContext();
const ActionsContext = createContext();
function MyContainer() {
const [contextState, setContextState] = useState();
return (
<ViewContext.Provider value={contextState}>
<ActionsContext.Provider value={setContextState}>
<MySetCtxComponent />
<MyViewCtxComponent />
</ActionsContext.Provider>
</ViewContext.Provider>
)
}
function MySetCtxComponent() {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
setContextState("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const contextState = useContext(ViewContext);
return (
<div>
This is the value of the context: {contextState}
</div>
);
}
ReactDOM.render(
<MyContainer />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The problem is what you're passing to the useEffect dependency array in MySetCtxComponent. You should only pass the update function as shown below.
However, personally I would destructure out the setter as it's more readable and naturally avoids this issue.
const { useState, createContext, useContext, useEffect, useRef, useCallback } = React;
const MyContext = createContext();
function MyContainer() {
const [myContextValue, setMyContextValue] = useState(null);
// this function is currently unnecessary, but left in because I assume you change the functions default behvaiour in your real code
// also this should be wrapped in a useCallback if used
const setCtxVal = useCallback((newVal: string) => {
setMyContextValue(newVal);
}, [setMyContextValue]);
return (
<MyContext.Provider value={{ value: myContextValue, setMyContextValue: setCtxVal }}>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
)
}
function MySetCtxComponent() {
const myCtx = useContext(MyContext);
// or const { setMyContextValue } = useContext(MyContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter.current);
// or setMyContextValue("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [myCtx.setMyContextValue, /* or setMyContextValue */]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the context: {myCtx.value}
</div>
);
}
ReactDOM.render(
<MyContainer />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

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 👍

Setting state without re-rendering with useEffect not working

I simply need my state value to change based on screen sizing live time. Even though my screen size changes, my count value stays the same unless I reload the page. this needs to update live for better responsive design. Here is my code. Thanks!
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive'
const MyCarousel = () => {
useEffect(() => {
loadMediaQuery();
}, []);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
if (phone)
setCount(2)
if (desktop)
setCount(3)
}
const tablet = useMediaQuery({
query: '(max-width: 876px)'
})
const phone = useMediaQuery({
query: '(max-width: 576px)'
})
const desktop = useMediaQuery({
query: '(min-width: 876px)'
})
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}
That's because your useEffect has no dependencies so it loads one time only after the component has been mounted.
To fix that you should have the following code:
import React, { useState, useEffect } from 'react';
import Carousel from '#brainhubeu/react-carousel';
import '#brainhubeu/react-carousel/lib/style.css';
import { useMediaQuery } from 'react-responsive';
const MyCarousel = () => {
const tablet = useMediaQuery({
query: '(max-width: 876px)'
});
const phone = useMediaQuery({
query: '(max-width: 576px)'
});
const desktop = useMediaQuery({
query: '(min-width: 876px)'
});
useEffect(() => {
loadMediaQuery();
}, [tablet, phone, desktop]);
const [count, setCount] = useState(0);
const loadMediaQuery = () =>{
if (tablet)
setCount(1)
else if (phone)
setCount(2)
else if (desktop)
setCount(3)
}
return (
<div>
<Carousel slidesPerPage={count} >
<img className="image-one"/>
<img className="image-two"/>
<img className="image-three"/>
</Carousel>
</div>
);
}
useEffect means: runs the callback function that's passed to useEffect after this component is rendered, and since you passed an empty array as the 2nd argument, it means useEffect is executed once, only the first time when this component gets rendered (and not when the state changes) , since your function call is inside useEffect, it will work only when you reload the page, you can either add the variables to the empty array like Mohamed Magdy did, or simply call loadMediaQuery again outside useEffect
Expanding on the custom hook provided in this answer, you can do something like
const [width, height] = useWindowSize();
useEffect(() => {
loadMediaQuery();
}, [width]);
That useEffect function says: Execute loadMediaQuery() every time the value of width changes.

How to avoid allocations in functional stateful components?

Consider the following component:
function SomeComponent(props){
const [isMouseOver, setIsMouseOver] = useState(false);
return (
<div onMouseOver={_ => setIsMouseOver(true)}
onMouseOut={_ => setIsMouseOver(false)}>
<img src={isMouseOver ? EditIconHover : EditIcon} alt="icon"/>
</div>
);
}
New instance of arrow function is created on every render. It creates a closure over setIsMouseOver function, though this function never changes.
Sure, it does not drastically affect performance in this case, but I'd like to know how to avoid these unnecessary memory allocations.
Do I have to attach all dependencies required for event handler to DOM element
<div data-deps={setIsMouseOver} onMouseOver={onMouseOverHandler} onMouseOut={onMouseOutHandler}></div>
and then access deps property inside onMouseOverHandler and onMouseOutHandler functions?
You might want to use memoization, although in this particular example its an overhead.
Refer to useCallback
import React, { useState, useCallback, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [isEnabled, setIsEnabled] = useState(false);
const toggle = useCallback(() => setIsEnabled(v => !v), []);
const toggleRef = useRef();
const setterRef = useRef();
useEffect(() => {
toggleRef.current = toggle;
setterRef.current = setIsEnabled;
}, [toggle]);
useEffect(() => {
console.log(toggle === toggleRef.current);
console.log(setIsEnabled === setterRef.current);
});
return <button onClick={toggle}>{isEnabled ? 'Enabled' : 'Disabled'}</button>;
};
ReactDOM.render(<App />, document.getElementById('root'));

Resources