How to use DOMRect with useEffect in React? - reactjs

I am trying to get the x and y of an element in React. I can do it just fine using DOMRect, but not in the first render. That's how my code is right now:
const Circle: React.FC<Props> = ({ children }: Props) => {
const context = useContext(ShuffleMatchContext);
const circle: React.RefObject<HTMLDivElement> = useRef(null);
const { width, height } = useWindowDimensions();
useEffect(() => {
const rect = circle.current?.getBoundingClientRect();
context.setQuestionPosition({
x: rect!.x,
y: rect!.y,
});
}, [width, height]);
return (
<>
<Component
ref={circle}
>
<>{children}</>
</Component>
</>
);
};
export default Circle;
The problem is that on the first render, domRect returns 0 to everything inside it. I assume this behavior happens because, in the first render, you don't have all parent components ready yet. I used a hook called "useWindowDimensions," and in fact, when you resize the screen, domRect returns the expected values. Can anyone help?

You should use useLayoutEffect(). It allows you to get the correct DOM-related values (i.e. the dimensions of a specific element) since it fires synchronously after all DOM mutations.
useLayoutEffect(() => {
const rect = circle.current?.getBoundingClientRect();
context.setQuestionPosition({
x: rect!.x,
y: rect!.y,
});
}, [width, height]);

Related

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

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

React State Updating after code requiring state change runs

I'm trying to create a grid of Nodes, which update when clicking/dragging on them. I'm running into a weird React State where the state is updating after the code following it runs.
Expected behaviour:
nodeTypePointer is set to 0 as default react state
Click on Blank Node, nodeTypePointer is set to 0 (unchanged) + logic uses 0
Click on Coloured Node, nodeTypePointer is set to 1 + logic uses 1
Actual behaviour
nodeTypePointer is set to 0 as default react state
Click on Blank Node, nodeTypePointer remains unchanged + logic uses 0
Click on Coloured Node for the first time, nodeTypePointer is set to 0 (unchanged) + logic uses 0, then after code is finished nodeTypePointer is set to 1
Click on Coloured Node for the second time, nodeTypePointer is now still set to 1, logic uses 1, then after code is finished nodeTypePointer is set to 0
I know that the state is async but unsure about the exact reason/problem/solution to go on. Unsure about how to pass parameters to the next functions if I were to use a UseEffect.
Here is the code:
import React, { useEffect, useState } from 'react';
import Node from './Node'
import './Grid.css';
const rectDiameter = 25
const gridHeightRatio = 0.9
const Grid: React.FC = () => {
const gridHeight: number = Math.floor(window.innerHeight * gridHeightRatio)
const gridWidth: number = window.innerWidth
const numX: number = Math.floor(gridHeight / rectDiameter)
const numY: number = Math.floor(gridWidth / rectDiameter)
const tempGrid: number[][] = [...Array(numX)].map(() => Array(numY).fill(0));
const [grid, setGrid] = useState(tempGrid)
const [isMousePressed, setMousePressed] = useState(false)
const [nodeTypePointer, setNodeTypePointer] = useState(0)
useEffect(() => {
console.log('Use Effect', nodeTypePointer)
}, [nodeTypePointer])
// Hacky el.ID to workaround React performance until I know how to do it better
const paintNode = (x: number, y: number) => {
const el = document.getElementById(`${x}-${y}`)
if (!el) return
if (nodeTypePointer === 0) {
el.classList.add("node-wall")
} else {
el.classList.remove("node-wall")
}
}
const updateGridPosition = (x: number, y: number) => {
const newValue = nodeTypePointer === 0 ? 1 : 0
let newGrid: number[][] = grid
newGrid[x][y] = newValue
setGrid(newGrid)
}
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>, x: number, y: number) => {
console.log('Node pointer type Before', nodeTypePointer)
setNodeTypePointer(grid[x][y])
console.log('Node pointer type After', nodeTypePointer)
setMousePressed(true)
console.log('Before updated Grid', grid[x][y])
updateGridPosition(x, y)
console.log('After updated Grid', grid[x][y])
paintNode(x, y) // <------------- Uses nodeTypePointer
}
const handleMouseUp = (event: React.MouseEvent<HTMLDivElement>, x: number, y: number) => {}
const handleHover = (event: React.MouseEvent<HTMLDivElement>, x: number, y: number) => {
if (!isMousePressed) return
updateGridPosition(x, y)
paintNode(x, y)
}
return (
<div className='gridContainer' >
{
grid.map((row: number[], i: number) =>
row.map((val: number, j: number) =>
<Node
x={i}
y={j}
d={rectDiameter}
key={`${i}-${j}`}
state={val}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onHover={handleHover} />
)
)
}
</div>
);
}
export default Grid
Any help on understanding the problem and suggested solutions would be greatly appreciated!
State changes are asynchronous (both component ones and hook ones). So your state hasn't update by the time you call paintNode. There's 2 ways I can think of around it. One is to put the call to paintNode in your nodeTypePointer's useEffect, like so:
useEffect(() => {
paintNode(x,y);
}, [nodeTypePointer])
but this means you'll need to store the x and y of clicked one in some standard variables, since you won't be able to pass them directly to useEffect. Another way (and maybe a better one, since I'm not a huge fan of state changes having side effects) is to just pass the x/y directly into paintNode during the handleMouseDown function:
paintNode(x, y, grid[x][y])
however it looks like updateGridPosition also needs the x/y, so maybe you would be better off storing the raw clicked one in state, then having a useEffect for the clicked one that does everything needed once a node is clicked. All depends on how this function is gonna end up looking.
But yeah, the reason for your bug is cause state changes are async so it hasn't updated by the time you call paintNode

React.useEffect stack execution prevents parent from setting defaults

I have attached a simplified example that demonstrates my issue:
https://codesandbox.io/s/reactusehook-stack-issue-piq15
I have a parent component that receives a configuration, of which screen should be rendered. the rendered screen should have control over the parent appearance. In the example above I demonstrated it with colors. But the actual use case is flow screen that has next and back buttons which can be controlled by the child.
in the example I define common props for the screens:
type GenericScreenProps = {
setColor: (color: string) => void;
};
I create the first screen, that does not care about the color (parent should default)
const ScreenA = (props: GenericScreenProps) => {
return <div>screen A</div>;
};
I create a second screen that explicitly defines a color when mounted
const ScreenB = ({ setColor }: GenericScreenProps) => {
React.useEffect(() => {
console.log("child");
setColor("green");
}, [setColor]);
return <div>screen B</div>;
};
I create a map to be able to reference the components by an index
const map: Record<string, React.JSXElementConstructor<GenericScreenProps>> = {
0: ScreenA,
1: ScreenB
};
and finally I create the parent, that has a button that swaps the component and sets the default whenever the component changes
const App = () => {
const [screenId, setScreenId] = useState(0);
const ComponentToRender = map[screenId];
const [color, setColor] = useState("red");
React.useEffect(() => {
console.log("parent");
setColor("red"); // default when not set should be red
}, [screenId]);
const onButtonClick = () => setScreenId((screenId + 1) % Object.keys(map).length)
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
<ComponentToRender setColor={setColor} />
</div>
);
};
In this example, the default color should be red, for screen A. and green for the second screen.
However, the color stays red because useEffect is using a stack to execute the code. if you run the code you will see that once the button clicked there will be child followed by parent in log.
I have considered the following solution, but they are not ideal:
each child has to explicitly define the color, no way to enforce it without custom lint rules
convert the parent into a react class component, there has to be a hooks solution
This might be an anti-pattern where child component controls how its parent behave, by I could not identify a way of doing that without replicating the parent container for each screen. the reason I want to keep a single parent is to enable transition between the screens.
If I understood the problem correctly, there is no need to pass down setColor to the children. Making expressions more explicit might make a bit longer code, but I think it helps in readability. As what you shared is a simplified example, please let me know if it fits your real case:
const ScreenA = () => {
return <div>screen A</div>;
};
const ScreenB = () => {
return <div>screen B</div>;
};
const App = () => {
const [screen, setScreen] = useState<"a" | "b">("a");
const [color, setColor] = useState<"red" | "green">("red");
const onButtonClick = () => {
if (screen === "a") {
setScreen("b");
setColor("green");
} else {
setScreen("a");
setColor("red");
}
};
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
{screen === "a" ? <ScreenA /> : <ScreenB />}
</div>
);
};
render(<App />, document.getElementById("root"));

Render React child component based on parent dimensions

I need to render a React child component from UV coordinates (normalized coordinates / U & V are typically in [0;1] range)
But I don't know how to get parent dimension during children rendering.
I would like to perform something like (using tsx):
const Child = (props: {u:Number, v:Number}) =>
<circle cx={parentClientWidth*props.u} cy={parentClientHeight*props.v} r="5" fill="black"></circle>;
const Parent = () =>
<svg>
<Child u={0.3} v={0.5} />
</svg>;
I woulder if using a context object could be the right way?...
const Child = (props: {u:Number, v:Number}) => {
const workspace= useContext(WorkspaceContext);
return <circle cx={workspace.width*u} cy={workspace.height*v} r="5"></circle>;
}
Note:
In this simple case I could use percents for my cx and cy coordinates but my real case is much more complicated...
After digging a lot the react documentation, I finally found a way that seems to me not too hacky... But I'm still learning so there might be another better way (regarding performances? readability?...)
Anyway here is the idea of my current solution:
// Shares the dimensions of the workspace through children
const WorkspaceContext = createContext();
/** A child component.
* It should be placed at specified (u,v) within the parent component.
*/
const Child = (props: {u:Number, v:Number}) => {
const workspaceContext = useContext(WorkspaceContext);
return <circle cx={workspaceContext.width*props.u} cy={workspaceContext.height*props.v} r="5" fill="black"></circle>;
};
/** The parent SVG component */
const Parent = () => {
const [dimensions, setDimensions] = useState({
width: undefined,
height:undefined,
outdated: true // Triggers a re render when changed
});
// I'm using the ref callback to get the svg dimensions
const parentRefCallback = (element: SVGSVGElement) => {
if(element) {
const width = element.clientWidth;
const height = element.clientHeight;
if(dimensions.width !== width || dimensions.height !== height) {
setDimensions({width, height, outdated: false});
}
}
};
useEffect(() => {
const handler = () => setDimensions({...dimensions, outdated: true}); // re renders!
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, [])
return <svg ref={parentRefCallback}>
<WorkspaceContext.Provider value={dimensions}>
<Child u={0.3} v={0.5} />
</WorkspaceContext.Provider>
</svg>;
}

Resources