How to force update state in ReactJS - javascript

I came from here so please don't mark it as a duplicate.
I'm trying to get every value of mainState to be placed in anotherState so at the end I have an array of arrays. mainState is an changes on hover and it changes a lot so I can't assign every value I have in it to anotherState, I only need the value when clicked.
const [mainState, setMainState] = useState([])
const [anotherState, setAnotherState] = useState([])
const [currentItem, setCurrentItem] = useState(0)
//mainState changes on hover and it can change a lot
const onClickHandler = () => {
setAnotherState([...anotherState, mainState])
if (currentItem >= 4) {
// done with this component move to the next component
} else {
setCurrentItem(currentItem + 1)
setMainState([])
}
}
return (
<div onClick={onClickHandler}></div>
However, this doesn't work and even though mainState updates perfectly everytime, anotherState is always one step behind. currentItem represents an array index so I can't click one more time to get the last value of mainState in anotherState. I , using useEffect() which broke React because hover changes a lot and I ended up with an array of hundreds of values.
// tried useEffect() like in the question above, ended up with an array of hundreds of values
useEffect(() => setAnotherState([...anotherState, mainState]))
// tried using callback function like in one of the comments which didn't work and remained the same
setAnotherState(() => [...anotherState, mainState])
// another different way not using hooks is I created an empty array constant in the component
// and on click I tried pushing the value of mainState to that array using deep copy
// I keep ending up with the last array pushed only
const arrays = []
const onClickHandler = () => {
arrays.push(JSON.parse(JSON.stringify(mainState)))
}
How can I work around this?

setAnotherState is asynchronous. So if you want to get latest update value of anotherState.
You should use useEffect like
useEffect(() =>
Console.log("anotherState",anotherState)
// Do your stuff
,[anotherState])
This hook will called whenever another state gets updated using setAnotherState.

Related

ReactJS push to state array replaces every exisitng value in array

I want to push an 2d array into the end of a state. But somehow, when i log the state every time it changes with a useEffect, i replaces every existing array with the new one.
The variable "position" is my 2d array, also a state. Might this cause any reference problems?
I have no idea and im starting to lose my mind about this...
const [positionList, setPositionList] = useState([]);
useEffect(() => {
const updatePosition = [...positionList, position]
setPositionList(updatePosition);
}, [playerTurn])
expected output (or what i want)
positionList = [position1, position2, position3, //and so on]
but what i get is this (lets say i try to push the array three times)
positionList = [position3, position3, position3]
EDIT:
So, after some good ideas from the community, i found out that i had to copy my array i want to push (the "position") before i push it into the state. So my working code looks now like this:
const [positionList, setPositionList] = useState([])
useEffect(() => {
let positionCopy = [];
for (let i = 0; i < position.length; i++) {
positionCopy[i] = position[i].slice();
}
setPositionList([...positionList, positionCopy]);
}
You didn't add positionList as a dependency of your useEffect callback, so the positionList captured by the callback doesn't necessarily reflect the latest value.
useEffect(() => {
// return early if the update already happened to prevent recursion
const updatePosition = [...positionList, position]
setPositionList(updatePosition);
}, [playerTurn, positionList])
TBH, using useEffect to update a state hook is difficult to maintain and prone to accidental recursive behaviour.
You might get better mileage by using a state reducer with the useReducer hook. For anything beyond trivial state management, the useReducer hook is worth getting to grips with.
try:
const [positionList, setPositionList] = useState([]);
useEffect(() => {
setPositionList(state=> [...state, position]);
}, [playerTurn])

What are my options to deal with this stale closure in React besides using ref

I'm using the library React Datasheet Grid, the main component has a property called OnActiveCellChange, to which i'm passing my custom function:
const [selectedCell, setSelectedCell] = useState([])
...
function handleCellSelection({ cell }) {
if(cell) {
const {row, col} = cell;
const newData = data[row][col];
if(!newData.unpickable) {
console.log(selectedCell); // prints empty array
setSelectedCell(selectedCell => [...selectedCell, newData.value]);
}
}
}
...
<DataSheetGrid
className={'overwriteDisabledStyles'}
value={data}
onChange={setData}
columns={columns}
onActiveCellChange={handleCellSelection}
headerRowHeight={false}
gutterColumn={false}
lockRows
/>
The problem is that I wish to make some checks with the state variable "selectedCell", while I managed to update the state from with the functional form, I can't access the the state variable. I managed to use the variable using "useRef".
I want to know if I can tackle the problem in any other way.
EDIT:
Trying to clarify my question:
What I want is to, inside the handleCellSelection function, make some checks before inserting the newData using the setSelectedCell. Like searching the selectedCell array for existing values:
const valueAlreadyExists = selectedCell.some((existingCell) => {
return existingCell === newData.value
});
Yet, when the handleCellSelection function is called, the value of selectedCell is always an empty array. It should be the values that are being inserted in the array. I know there are values there because when I console.log outside the handleCellSelection they appear correctly.
So, I managed to access them inside the handleCellSelection using useRef() but what I am searching for is another option to solve this.

Is there a way in React.js to run a side effect after `useState` when the value of the state doesn't change in functional components?

One thing I've recently been confused about is what the best way to run effects after a useState call, when the useState doesn't necessarily change the previous state value. For example, say I have a submit function in a form. Also say I have a state called randomNumber, and say in that submit function I call setRandomNumber and set the random number state to a random number from 1-2. Now say that for some reason every time the randomNumber is set, regardless of whether its value changes or not, we want to update the number in some database and navigate to a different page. If I use a useEffect(() => {updateDatabase(); }, [randomNumber]), the problem is that this will update the database even when the functional component first renders, which I do not want. Also, if the randomNumber doesn't change because we pick the same random number twice, we won't update the database after the second setRandomNumber call and we won't navigate to a different page which I don't want.
What would be the best way to 'await' a useState call. I read some articles saying we should potentially use flushSync, but I tried this and it doesn't seem to be working and it seems like this is a very unstable solution as of now.
Thanks!!!
You have 2 different requests here.
How to skip a useEffect on the first render. For this you could employ useRef as this does not trigger a re-render
const Comp = () => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return
} else {
// do your stuff
}
}, [randomNumber])
}
How to trigger useEffect even if you get the same number. Probably the simplest way would be to wrap your number inside an object and always generate a new object when generating a new number. This way you'll get a new ref all the time and the useEffect will always trigger. If you already have an object you could create a copy using spread operator. Something like this:
const Comp = () => {
const [randomNumber, setRandomNumber] = useState({number: 0})
useEffect(() => {
console.log(randomNumber.number)
}, [randomNumber])
// generate a new random number and set it like this
setRandomNumber({number: Math.random() + 1))
}

useSelector does not work when I set two state inside?

Why doesn't my code work? I am trying to separate these two things into one, but useSelector does not work.
This code works
const st = useSelector((state) =>
state.or.st
);
const ter = useSelector(
({ main }) => main.st.ter
);
This code does not work
I want to put these two things in one useSelector and not in two?
Issue 1: Incorrect Function Usage
The useSelector hook expects a function that takes a single parameter state.
state is the whole state held by redux.
This would mean that your combine function should look more like the following:
const { units, organistationTerminology } = useSelector(state => {
organistationTerminology = state.main.organisation.terminology;
units = state.organisationStructure.units.sort(
(a, b) => a.sortOrder - b.sortOrder
);
return {
organistationTerminology,
units
}
});
Issue 2: Changing Reference
The combined selector function returns a new object which will have a difference reference each time the selector function is computed.
The default comparison used by useSelector to detect a change between values is a reference comparison.
This will result in your component being rerendered on any store change regardless of whether state.main.organisation.terminology or state.organisationStructure.units is updated.
Issue 3: Computation in Selector
Selectors will run on each store change and the resulting value will be compared with the previous value to determine whether a component rerender needs to be triggered.
This means that even if state.organisationStructure.units never changes you will still be running a sort on every store change.
Computations should, where possible, be preformed outside of the selector function.
Issue 4: Sort on State Value
sort() sorts the array in place which would be mutating your state.
Conclusion
Selector functions should be kept as simple / quick as possible as they will be run on every store change.
Pulling multiple values from separate sections of state in one selector usually adds more complexity than it is worth.
My suggestion would be to keep it simple:
const units = useSelector(state => state.organisationStructure.units);
const organistationTerminology = useSelector(state => state.main.organisation.terminology);
const sortedUnits = [...units].sort();
// OR the sort can be memoized using units as a dependency;
const sortedUnits = useMemo(() => [...units].sort(), [units]);
If this is something you will be reusing in a number of components you could wrap it up in a custom hook:
const useTerminologyAndSortedUnits = () => {
const units = useSelector(state => state.organisationStructure.units);
const terminology = useSelector(state => state.main.organisation.terminology);
const sortedUnits = useMemo(() => [...units].sort(), [units]);
return {
units: sortedUnits,
terminology
}
};
// The following can then be used in any of your function components
const { units, terminology } = useTerminologyAndSortedUnits()

Call function only after multiple states have completed updating

Logic:
I have a dialog for converting units. It has two stages of choice for the user: units to convert from and units to convert to. I keep this stage as a state, dialogStage, for maintainability as I'm likely going to need to reference what stage the dialog is in for more features in the future. Right now it's being used to determine what action to take based on what unit is clicked.
I also have a state, dialogUnits, that causes the component to rerender when it's updated. It's an array of JSX elements and it's updated via either foundUnitsArray or convertToUnitsArray, depending on what stage the dialog is at. Currently both states, dialogStage and dialogUnits, are updated at the same moment the problem occurs.
Problem:
When choosing the convertTo units, displayConversionTo() was still being called, as though dialogStage was still set to 'initial' rather than 'concertTo'. Some debugging led to confusion as to why the if (dialogStage == 'initial') was true when I'd set the state to 'convertTo'.
I believe that my problem was that the dialogStage state wasn't updated in time when handleUnitClick() was called as it's asynchronous. So I set up a new useEffect that's only called when dialogStage is updated.
The problem now is that the dialog shows no 'convertTo' units after the initial selection. I believe it's now because dialogUnits hasn't updated in time? I've swapped my original problem from one state not being ready to another state not being ready.
Question
How do I wait until both states are updated before continuing to call a function here (e.g. handleUnitClick()?).
Or have I mistaken what the problem is?
I'm new to react and, so far, I'm only familiar with the practice of state updates automatically rerendering a component when ready, unless overridden. Updating dialogUnits was displaying new units in the dialog until I tried to update it only when dialogStage was ready. It feels like an either/or situation right now (in terms of waiting for states to be updated) and it's quite possible I've overlooked something more obvious, as it doesn't seem to fit to be listening for state updates when so much of ReactJs is built around that already being catered for with rerenders, etc.
Component code:
function DialogConvert(props) {
const units = props.pageUnits;
const [dialogUnits, setDialogUnits] = useState([]);
const [dialogStage, setDialogStage] = useState('initial');
let foundUnitsArray = [];
let convertToUnitsArray = [];
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
useEffect(() => {
setDialogUnits(foundUnitsArray);
}, []);
useEffect(() => {
if (dialogStage == "convertTo") {
setDialogUnits(convertToUnitsArray);
}
}, [dialogStage]);
function handleClickClose(event) {
setDialogStage('initial');
props.callbackFunction("none");
}
function handleUnitClick(homogName) {
if (dialogStage == "initial") {
// getConversionChoices is an external function that returns an array. This returns fine and as expected
const choices = getConversionChoices(homogName);
displayConversionTo(choices);
} else if (dialogStage == "convertTo") {
// Can't get this far
// Will call a function not displayed here once it works
}
}
function displayConversionTo(choices) {
let canConvertTo = choices[0]["canconvertto"];
if (canConvertTo.length > 0) {
canConvertTo.forEach(element => {
convertToUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
setDialogStage('convertTo');
}
}
return (
<React.Fragment>
<div className="dialog dialog__convertunits" style={divStyle}>
<h2 className="dialogheader">Convert Which unit?</h2>
<div className='js-dialogspace-convertunits'>
<ul className="list list__convertunits">
{dialogUnits}
</ul>
</div>
<button className='button button__under js-close-dialog' onClick={handleClickClose}>Close</button>
</div>
</React.Fragment>
)
}
So, there are some issues with your implementations:
Using non-state variables to update the state in your useEffect:
Explanation:
In displayConversionTo when you run the loop to push elements in convertToUnitsArray, and then set the state dialogStage to convertTo, you should be facing the issue that the updated values are not being rendered, as the change in state triggers a re-render and the convertToUnitsArray is reset to an empty array because of the line:
let convertToUnitsArray = [];
thus when your useEffect runs that is supposed to update the
dialogUnits to convertToUnitsArray, it should actually set the dialogueUnits to an empty array, thus in any case the updated units should not be visible on click of the initial units list.
useEffect(() => {
if (dialogStage == "convertTo") {
// as your convertToUnitsArray is an empty array
// your dialogue units should be set to an empty array.
setDialogUnits(convertToUnitsArray)
}
}, [dalogStage]);
You are trying to store an array of react components in the state which is not advisable:
http://web.archive.org/web/20150419023006/http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-components-should-have-state
Also, refer https://stackoverflow.com/a/53976730/10844020
Solution: What you can do is try to save your data in a state, and then render the components using that state,
I have created a code sandbox example how this should look for your application.
I have also made some changes for this example to work correctly.
In your code , since you are passing units as props from parent, can you also pass the foundUnitsArray calculated from parent itself.
setDialogUnits(props.foundUnitsArray);
and remove the below operation,
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});

Categories