React, useEffect only when 2 elements of dependency array are updated - reactjs

I have a React table component where both Column component and a button component rendered inside a column, triggers some function. Example of the table:
<Table>
<Column onCellClick={handleCell}>
<button onClick={handleButton} />
</Column>
</Table>
Those 2 handle functions are called at the same click, and they trigger some useStates:
const [cell, setCell] = useState(false);
const [buttonValue, setButtonValue] = useState(false);
const handleCell = (value) => setCell(value)
const handleButton = (value) => setButtonValue(value)
SO I have a useEffect that must trigger some code ONLY when both cell and buttonValue are updates. Currently I have something like:
useEffect(() => {
if (cell && buttonValue) {
// some code
setAnotherState(etc)
}
}, [cell, buttonValue]);
My problem is if I click very quickly or in random scenarios, the setAnotherState is called before the buttonValue or cell are actually updated. With that if inside the useEffect, it will work correctly only the very first time that I actually update both values, because they both are initialized as false, but then, with many clicks, there are some outdated set states.
Any hint? how could I ensure that both states actually update before executing the code inside the if?
I can't remove any of that 2 onClick or onCellClick, they both have different and specific values for their components, so I need them both.

Here's an idea using useRef to store the collective state as an integer value you can test against.
Cell adds 2 to the state (first taking a modulus of 2 to remove existing 2 values), Button adds 1 (first doing a bit-shift and x2 to eliminate the first bit). When the stateRef is 3, the effect knows both have been changed, sets the other state, and resets the stateRef to 0.
const [cell, setCell] = useState(false);
const [buttonValue, setButtonValue] = useState(false);
const stateRef = useRef(0);
const handleCell = (value) => {
setCell(value);
stateRef.current = stateRef.current % 2 + 2;
};
const handleButton = (value) => {
setButtonValue(value);
stateRef.current = (stateRef.current >> 1) * 2 + 1;
};
Then
useEffect(() => {
if (stateRef.current == 3) {
// some code
setAnotherState(etc)
stateRef.current = 0;
}
}, [stateRef.current]);

You could do something like this:
const [cell, setCell] = useState(null);
// it stores the updated status for cell
const [cellStatus, setCellStatus] = useState(false);
const [buttonValue, setButtonValue] = useState(null);
// it stores the updated status for buttonValue
const [buttonValueStatus, setButtonValueStatus] = useState(false);
const handleCell = (value) => {
setCell(value)
setCellStatus(true) // it carried a updated event
}
const handleButton = (value) => {
setButtonValue(value)
setButtonValueStatus(true) // it carried a updated event
}
useEffect(() => {
// !!(null) === false
if (!!cellStatus && !!buttonValueStatus) {
// some code
setAnotherState(etc)
// here you reset their status since they were already updated and used
// now they are not new values but old ones
setCellStatus(false)
setButtonValueStatus(false)
}
}, [cell, buttonValue]);
Thus, the basic idea is to keep track of the values in one variable and the update status in another one.

Related

React useEffect trigger conditionally

I'm moving over to functional components from class components and having trouble handling this scenario. This component's state can be updated by 2 different event listeners: key down or mouseover. I want to trigger a callback after the state is updated ONLY if updated by key down. Any way to do this?
const handleMouseOver = e => {
setSelection(e.target.value)
}
const handleDownArrowKeyDown = () => {
...
setSelection(selection + 1)
}
useEffect(() => {
// Only execute below if selection state was updated by handleDownArrowKeyDown
...
}, [selection])
Put that code inside handleDownArrowKeyDown instead - accounting for the new selection number.
const handleMouseOver = e => {
setSelection(e.target.value)
}
const handleDownArrowKeyDown = () => {
setSelection(newSelection)
const newSelection = selection + 1;
// stuff that relies on newSelection
}

Implementing undo/redo function in react

I am trying to implement simple undo/redo function in my react app. So I am trying to maintain an array with old state values. But when I check the values of the old states, its all updated with the new state.
state :
state = {
schedule : [],
loads:[],
undo:[],
redo:[]
};
const redoUndoObj ={
oldStateSchedule : [...this.state.schedule],
oldStateLoads : [...this.state.loads]
}
this.setState({ undo : [ ...this.state.undo , redoUndoObj]});
I hope this give you an idea on how to solve the problem. I made code only for undo now to point you in the right direction. This example I made via React functional component using useState instead of Component class.
const [schedule, setSchedule] = useState([]);
const [loads, setLoads] = useState([]);
const [undo, setUndo] = useState([]);
const [redo, setRedo] = useState([]);
const updateData = (newSchedule, newLoads) => {
setSchedule([...newSchedule]);
setLoads([...newLoads]);
const newUndo = {
schedule: [...newSchedule],
loads: [...newLoads],
};
setUndo([...undo, ...newUndo]);
}
const undoChanges = () => {
const lastElement = undo[undo.length - 1];
const copyOfUndo = [...undo];
// Update redo to be able to rollback
setRedo([...undo]);
// Set the previous values to Schedule and Loads
schedule([...lastElement.schedule]);
loads([...lastElement.loads]);
// Remove the last element from undo
lastElement.pop();
undo([...lastElement]);
}

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:

React hooks toggle setState

so far I saw two ways to handle toggle
first, set state based on previous state
export default function App() {
const [show, setShow] = useState(false);
const handleClick = () => {
setShow(s => !s);
};
return (
<div>
<div>show: {String(show)}</div>
<button onClick={handleClick}>BTN</button>
</div>
);
}
second, pass the state directly as the argument
export default function App() {
const [show, setShow] = useState(false);
const handleClick = () => {
setShow(!show);
};
return (
<div>
<div>show: {String(show)}</div>
<button onClick={handleClick}>BTN</button>
</div>
);
}
which one is correct? In my understanding the second may not work as set state is async, it may not be able to get the correct state if I click the button several times
The first one is correct:
setShow(s => !s);
Although the second one works in this case it is not correct:
// bad code
setShow(!show);
Why?
Setting state is asynchronous - but what that really means is that setting state is deferred until re-render.
An example:
const [myNum, setMyNum] = useState(0)
const handleUpdate = () => {
setMyNum(myNum + 1)
}
If we call the handleUpdate function, the new value of myNum is incremented by 1. That works correctly, but what about this:
const [myNum, setMyNum] = useState(0)
const handleUpdate = () => {
setMyNum(myNum + 1)
setMyNum(myNum + 1)
}
You may think that myNum is now 2, but it's not, it's still 1.
This happens because the value of myNum does not change until re-render, so you're essentially writing this:
const handleUpdate = () => {
setMyNum(0 + 1)
setMyNum(0 + 1)
}
That is why you should always provide a function to set state if you depend on the previous value.
This works correctly:
const handleUpdate = () => {
setMyNum(p => p + 1)
setMyNum(p => p + 1)
}
myNum is now 2.
Even if you're only setting the value once, you should maintain good practice. When your app gets complex and several different actions may set the state at any time, you don't want to be caught out by something you wrote months ago that you never thought was going to matter.

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