React State Updating after code requiring state change runs - reactjs

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

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

How to use DOMRect with useEffect in React?

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]);

How to synchronously change state within useState

Hey guys I am new to using React Hooks but couldn't find it on google directly. I am attempting to nest setState callback so that the state can update synchronously but haven't had success as I get undefined values. The values within the state are reliant on other values within my state so I would ideally like it to run synchronously. I tried nesting it below, which works when it is a class component and use this.setState and use the callback, but doesn't work when I attempt to use react hooks within a functional class.
Here is my code:
const [state, setState] = useState({numCols: 0, numRows: 0, cardWidth: 0, cardHeight: 0, containerWidth: 0});
const {
sheetList,
sheetsTotalCount,
sheetsMetadata,
id,
projectId,
onEditButtonClick,
handleInfiniteLoad,
sheetsAreLoading,
classes,
permissions,
onItemClick,
} = props;
const setCardSize = ({ width }) {
setState({
containerWidth: width > 0 ? width : defaultWidth
},() => {
setState({numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }, () => {
setState({numRows: Math.ceil(sheetList.size / state.numCols)}, () => {
setState({cardWidth: Math.floor(state.containerWidth / state.numCols - 2 * marginBetweenCards)}, () => {
setState({cardHeight: Math.round(state.cardWidth * thumbnailProportion)});
});
});
});
});
}
Ideally I would like the containerWidth variable to update first, then the numCols variable, then the cardWidth, then the cardHeight. Is there any way to do this synchronously so I don't get an undefined value?
Seeing as you're calculating a load of variables that are dependant upon another, and you want the state to all update at the same time, why not split the calculations out and set state once at the end? Much more readable, and only need one setState call.
const setCardSize = ({ width }) => {
const containerWidth = width > 0 ? width : defaultWidth;
const numCols = Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1,);
const numRows = Math.ceil(sheetList.size / numCols);
const cardWidth = Math.floor(containerWidth / numCols - 2 * marginBetweenCards);
const cardHeight = Math.round(cardWidth * thumbnailProportion);
setState({ containerWidth, numCols, numRows, cardWidth, cardHeight });
};
To answer the actual question though, if you want to cause the state update of one variable to immediately (or "synchronously" as you put it) update another state variable, then use useEffect.
You just give useEffect two parameters: a function to run every time a dependant variable changes, and then an array of those variables to keep an eye on.
It is cleaner (and faster, less bug-prone, and generally recommended for functional components) for each state variable to have its own useState, rather than just one large object, which I have also done here.
const [containerWidth, setContainerWidth] = useState(0);
const [numCols, setNumCols] = useState(0);
const [numRows, setNumRows] = useState(0);
const [cardWidth, setCardWidth] = useState(0);
const [cardHeight, setCardHeight] = useState(0);
const setCardSize = ({ width }) => setContainerWidth(width > 0 ? width : defaultWidth)
useEffect(() => setNumCols(Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1)) , [containerWidth])
useEffect(() => setNumRows(Math.ceil(sheetList.size / numCols)), [numCols])
useEffect(() => setCardWidth(Math.floor(containerWidth / numCols - 2 * marginBetweenCards)), [containerWidth])
useEffect(() => setCardHeight(Math.round(cardWidth * thumbnailProportion)), [cardWidth])
I'm sort of confused on what you want to achieve. But don't forget, unlike a class you have to set all properties a in state each time.
setState({numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }
This code will replace all your state with just numCols. You want the rest of state in there like this, now only numCols will change, everything else will be the same.
setState({...state, numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }
Next thing to remember is if you want to change state multiple time in one render use this form:
setState(oldState => {...oldState, newValue: 'newValue'});
This will allow for multiple updates to state in one render with the last value set instead of on the last render. For example:
const [state, setState] = useState(0); // -> closed on this state's value!
setState(state + 1);
setState(state + 1);
setState(state + 1); //State is still 1 on next render
// because it is using the state which happened on the last render.
// When this function was made it "closed" around the value 0
// (or the last render's number) hence the term closure.
vs this:
const [state, setState] = useState(0);
setState(state => state + 1);
setState(state => state + 1);
setState(state => state + 1); //State is 3 on next render.
But why not just calculate the values synchronously?
const setCardSize = (width) => {
const containerWidth = width > 0 ? width : defaultWidth;
const numCols = Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1);
const numRows = Math.ceil(sheetList.size / numCols);
const cardWidth = Math.floor(containerWidth / numCols - 2 * marginBetweenCards);
const cardHeight = Math.round(cardWidth * thumbnailProportion);
setState({containerWidth, numCols, numRows, cardWidth, cardHeight});
}
Check out the docs it discusses
Unlike the setState method found in class components, useState does
not automatically merge update objects.
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value. Here’s an example of a counter component
that uses both forms of setState:

State getting reset to 0 after render

I am rendering photos from unsplash api. And I am keeping the index of the photos to be used in the lightbox, after the initial render state of imageindex goes back to 0, how can I retain its value?
I will show some code
const ImageList = ({ image, isLoaded }) => {
const [imageIndex, setImageIndex] = useState(0);
const [isOpen, setIsOpen] = useState('false');
const onClickHandler = (e) => {
setIsOpen(true);
setImageIndex(e.target.id);
};
const imgs = image.map((img, index) => (
<img
id={index}
key={img.id}
src={img.urls.small}
onClick={onClickHandler}
if (isOpen === true) {
return (
<Lightbox
onCloseRequest={() => setIsOpen(false)}
mainSrc={image[imageIndex].urls.regular}
onMoveNextRequest={() => setImageIndex((imageIndex + 1) % image.length)}
onMovePrevRequest={() => setImageIndex((imageIndex + image.length - 1) % image.length)}
nextSrc={image[(imageIndex + 1) % image.length].urls.regular}
prevSrc={image[(imageIndex + image.length - 1) % image.length].urls.regular}
/>
after the initial render state, imageIndex goes back to 0.
That makes sense, the initial render would use whatever you set as the default value. You can use something like local storage to help you keep track of the index of the last used item. It's a bit primitive, but until you integrate something like Node/MongoDB for database collections, this will be perfect.
In your component, import useEffect() from React. This hook lets us execute some logic any time the state-index value changes, or anything else you might have in mind.
import React, { useEffect } from "react"
Then inside your component, define two useEffect() blocks.
Getting last used index from localStorage on intitial load:
useEffect(() => {
const lastIndex = localStorage.getItem("index", imageIndex)
setImageIndex(imageIndex)
}, []) //set as an empty array so it will only execute once.
Saving index to localStorage on change:
useEffect(() => {
localStorage.setItem("index", imageIndex)
}, [imageIndex]) //define values to subscribe to here. Will execute anytime value changes.

What is the correct way to use react hook useState update function?

Considering the following declaration:
const [stateObject, setObjectState] = useState({
firstKey: '',
secondKey: '',
});
Are the following snippets both corrects ?
A)
setObjectState((prevState) => ({
...prevState,
secondKey: 'value',
}));
B)
setObjectState({
...stateObject,
secondKey: 'value',
}));
I am sure that A) is correct, but is it necessary ? B) seems ok, but as setObjectState is an asynchronous function, stateObject might not have the most recent value.
One useful thing about case of A that I have found is that you can use this method to update state from child components while only passing down a single prop for setObjectState. For example, say you have parent component with state you would like to update from the child component.
Parent Component:
import React, {useState} from 'react';
import ChildComponent from './ChildComponent';
export const ParentComponent = () => {
const [parentState, setParentState] = useState({
otherValue: null,
pressed: false,
});
return (
<ChildComponent setParentState={setParentState} />
)
}
Child Component:
import React from 'react';
export const ChildComponent = (props) => {
const callback = () => {
props.setParentState((prevState) => ({
...prevState
pressed: true,
}))
}
return (
<button onClick={callback}>test button<button>
)
}
When the button is pressed, you should expect to see that the state has been updated while also keeping its initial values. As for the difference between the two, there isn't much as they both accomplish the same thing.
A will always give you the updated value. B could be correct but might not. Let me give an example:
const Example = props => {
const [counter, setCounter] = useState(0);
useEffect(() => {
// 0 + 1
// In this first case the passed value would be the same as using the callback.
// This is because in this cycle nothing has updated counter before this point.
setCounter(counter + 1);
// 1 + 1
// Thanks to the callback we can get the current value
// which after the previous iexample is 1.
setCounter(latest_value => latest_value + 1);
// 0 + 1
// In this case the value will be undesired as it is using the initial
// counter value which was 0.
setCounter(counter + 1);
}, []);
return null;
};
When the new value depends on the updated one use the callback, otherwise you can simply pass the new value.
const Example = props => {
const [hero, setHero] = useState('Spiderman');
useEffect(() => {
// Fine to set the value directly as
// the new value does not depend on the previous one.
setHero('Batman');
// Using the callback here is not necessary.
setHero(previous_hero => 'Superman');
}, []);
return null;
};
Also in the example you are giving it would probably be better to use two different states:
const [firstKey, setFirstKey] = useState("");
const [secondKey, setSecondKey] = useState("");

Resources