I'm trying to setup an onKeyPress event listener and I'm confused as to why the initial value is undefined and then the value I want. The data is added on mount (see x in console). Why am I unable to immediately capture it and instead get an initial undefined, especially since it clearly already exists in state?
useEffect(() => {
console.log('x', multipleChoice); <-- logs the array of objects
const handleKeyPress = ({ key }) => {
const index = Number(key) - 1;
if (key === '1') {
console.log(multipleChoice[index]); <-- logs undefined, then logs object
}
};
window.addEventListener('keydown', (e) => handleKeyPress(e));
return () => {
window.removeEventListener('keydown', (e) => handleKeyPress(e));
};
}, [allCards, currentCard, multipleChoice]);
LocalState
const [currentCard, setCard] = useState(0);
const [multipleChoice, setMultipleChoice] = useState([]);
// allCards is passed as a prop on page load from the parent
When the user guesses an answer correctly the currentCard is incremented by 1
UseEffect that sets multipleChoice
useEffect(() => {
const generateMultipleChoice = (words: Word[]) => {
const possibleAnswers = words.reduce(
(accum: Word[]) => {
while (accum.length < 4) {
// randomly select words from pool
const index = getRandomInt(0, allCards.length - 1);
const randomWord = allCards[index];
// verify current hand doesn't already have that word
if (!accum.includes(randomWord)) {
accum.push(randomWord);
}
}
return accum;
},
// default with the current card already in the hand
[allCards[currentCard]]
);
// return the hand with the matching card and (3) other cards from pool
return possibleAnswers;
};
const shuffledCards = shuffle(generateMultipleChoice(allCards));
setMultipleChoice(shuffledCards);
}, [allCards, currentCard]);
screenshot of console
This is it:
// initial state
const [multipleChoice, setMultipleChoice] = useState([]);
// therefore, initially, if index is any Number
console.log(multipleChoice[index]) // undefined
The object is returned, only until the calculation is finished...
useEffect(() => {
// ...
// this has to run before `multipleChoice` is updated to return that object
const shuffledCards = shuffle(generateMultipleChoice(allCards));
setMultipleChoice(shuffledCards);
}, [allCards, currentCard]);
Related
I have a group of functions that are responsible for adding and removing sections from my form in my app. I use a counter (subjectAddressCounter) in my state that keeps track of the iteration of the section being added. This works as expected when the user clicks the buttons to add and remove the sections, however when I call the addSection() function on init in a form mapping function generateAdditiveFormKeys() the counter jumps from 0 to 2 and so it only calls the callback function in the useEffect responsible for adding the section one time, which means my form doesn't build correctly.
So to be clear. I expect that for each time the function is called the counter will iterate by one, but what's happening on init is that my function is called twice, but in the useEffect the counter goes from 0 to 2 instead of 0, 1, 2. This causes my callback function to only be called once and then the sections are out of sync.
What am I doing wrong?
Please see the relevant code below. Also, is there a way to simplify this pattern so I don't need to use three functions just to add or remove a section?
const FormGroup = prop => {
const [subjectAddressCounter, setSubjectAddressCounter] = useState(0);
const addValues = () => {
const newSection = subjectAddressCopy.map(copy => {
const clone = {
...copy,
prop: copy.prop = `${copy.prop}_copy_${subjectAddressCounter}`,
multiTypes: copy.multiTypes ? copy.multiTypes.map(multi => {
const cloneMultiTypes = {
...multi,
prop: multi.prop = `${multi.prop}_copy_${subjectAddressCounter}`,
};
return cloneMultiTypes;
}) : null,
};
return clone;
});
...
};
const removeValues = () => {
...
}
const prevSubjectAddressCounterRef = useRef<number>(subjectAddressCounter);
useEffect(() => {
prevSubjectAddressCounterRef.current = subjectAddressCounter;
if (prevSubjectAddressCounterRef.current < subjectAddressCounter) {
// call function to addValues
// this is only being called once on init even though subjectAddressCounter starts at 0 and goes to 2
addValues();
}
if (prevSubjectAddressCounterRef.current > subjectAddressCounter) {
// call function to removeValues
removeValues();
}
}, [subjectAddressCounter]);
const addSection = section => {
if (section.section === SectionTitle.subjectAddress) {
// this gets called twice on init
setSubjectAddressCounter(prevCount => prevCount + 1);
}
};
const removeSection = section => {
if (section.section === SectionTitle.subjectAddress) {
setSubjectAddressCounter(prevCount => prevCount - 1);
}
};
const generateAdditiveFormKeys = reportResponse => {
const {
entity_addresses_encrypted: entityAddressesEncrypted, // length equals 3
} = reportResponse;
let additiveAddresses = {};
if (entityAddressesEncrypted?.length > 1) {
entityAddressesEncrypted.forEach((entityAddress, i) => {
if (i === 0) return;
if (subjectAddressCounter < entityAddressesEncrypted.length - 1) {
addSection({ section: SectionTitle.subjectAddress });
}
const keysToAdd = {
[`question_11_address_copy_${i - 1}`]: entityAddress.address,
[`question_12_city_copy_${i - 1}`]: entityAddress.city,
[`question_13_state_copy_${i - 1}`]: entityAddress.state,
[`question_14_zip_copy_${i - 1}`]: entityAddress.zip,
[`question_15_country_copy_${i - 1}`]: entityAddress.country,
};
additiveAddresses = { ...additiveAddresses, ...keysToAdd };
});
}
...
}
return (
...
button onClick={() => addSection(form)}
button onClick={() => removeSection(form)}
)
}
I have an application the receives new data over a WebSocket every second. Each second I receive 10 to 15 messages that I need to store in and display. I am currently updating a state array each time I receive new data but the effect is that I re-render the screen 10 to 15 times per second.
What I want to achieve is to store the incoming data in an array but only update the screen once every second.
My approach that I can't get working is to create a non-state array that is updated when new data is received and copy that data to a state array every second with a timer.
This is the declaration of the state array:
const [boatData2, _setBoatData2] = useState({});
const boatDataRef = useRef(boatData2);
const setBoatData2 = (update) => {
boatDataRef.current = update;
_setBoatData2(update);
}
This is the hook code where the data is received:
useEffect(() => {
if (!ws.current) return;
ws.current.onmessage = e => {
setDataFlowing(true);
setDataAge(0);
setScreenUpdates(screenUpdates => screenUpdates + 1);
//console.log('New Data');
const message = JSON.parse(e.data);
if (message.updates && message.updates.values) {
message.updates[0].values.forEach(obj => {
let newPath = obj.path.split('.').join("");
const update = {
path: obj.path,
value: obj.value,
timestamp: message.updates[0].timestamp,
valid: true,
age: 0,
};
now = Date.parse(message.updates[0].timestamp);
setBoatData2({ ...boatDataRef.current, [newPath]: update });
});
}
};
}, []);
This is the code that runs every second:
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
let boatdata = boatData2;
//console.log(boatData3);
Object.values(boatdata).forEach(val => {
val.age = val.age + 1;
if (val.age > 30) {
val.valid = false;
}
});
setBoatData2(boatdata);
setDataAge(dataAge => dataAge + 1);
if (dataAge > 60) {
setDataFlowing(false);
}
}, 1000);
} else if (!isActive && seconds !== 0) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isActive, seconds, boatData2]);
You can do this with the help of useRef .
const messageRef = useRef([]);
This creates a object inside messageRef called current which we can mutate and mutating it will not trigger a re-render. Now your messageRef will be something like this
{
current: []
}
Now whenever you get the message from the websocket push the message into this ref as
messageRef.current.push(your message)
Now inside your function which updates the state after some xyz second . You can use this Ref to update the state
setYourMessages(messageRef.current);
messageRef.current = []; // do this after you state update call. Else you will be pushing duplicate messages into the state
React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);
In my current I am was trying to check if the latest index (number 5) jumps to 1. Since I have built a function counter that automatically jumps to 1 when it reach the latest index, but I also want to have a check when it jumps from latest index to the first index...React Hook not necessarily needed for this issue....
const App = ({ scoreCounter }) => {
const boolean = useRef(null);
const [ checkCounter, setCheckCounter ] = useState(false);
useEffect(() => {
const storedCounter = currentCounter;
boolean.current = storedCounter;
return () => storedCounter;
}, []);
useEffect(() => {
if(currentCounter == 5) {
}
console.log(boolean.current, currentCounter);
}, [boolean.current, currentCounter])
}
const mapStateToProps = state => {
return {
currentCounter: state.game.counter
}
}
If you're using class components you can compare your prevState to your current state in the componentDidUpdate function. In hooks you can implement something similar with the example shown here: usePrevious.
Using the usePrevious function in the link you can do this:
const prevCount = usePrevious(checkCounter);
// Hook
function usePrevious(value) {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
useEffect(() => {
if (prevCount === 5 && checkCounter === 1) {
// your code here
}
}, [prevCount, checkCounter])
I need to initialize a state array with six elements, points, and then update the proper element in the points array when setVote function is called:
const [selected, setSelected] = useState(0)
const setNextItem = (value) => setSelected(value)
const initialValue = new Array(6).fill(0);
const [points, setPoint] = useState(initialValue)
const setVote = (value) => setPoint(value)
// randomly choose the next item
const setNextitem(Math.floor((Math.random() * 5) + 0))
const setVote => {
const copy = [...points]
copy[selected] += 1
return copy;
}
However, when I view points and copy arrays in the console window, points never gets updated with copy. It stay as Array(6) [ 0, 0, 0, 0, 0, 0 ]. Is it always initializing or is the copy array not retuned. What is the correct way to do this?
Here is my final code:
```
const App = () => {
const [selected, setSelected] = useState(0)
const [points, setPoints] = useState(new Array(6).fill(0))
const setNextItem = (value) => setSelected(value)
const setVoteAt = (value) => setPoints(value)
console.log(selected, points)
const next_button = {
name : 'next item',
onClick: () => setNextItem(Math.floor((Math.random() * 5) + 0))
}
const vote_button = {
name : 'vote',
onClick: () => setVoteAt(UpdatePoints(selected))
}
const UpdatePoints = (index) => {
let copy = [...points]
copy[index]++
return copy
}
```
Too many duplicated function and state
Just a little order the code..
react useState is not rective, then you can do useEffect to console.
And then you will see the update array.
const [points, setPoints] = useState(new Array(6).fill(0));
const UpdatePoints = (selected) => {
let copy = [...points];
copy[selected]++;
setPoints(copy);
}
const random = () => Math.floor((Math.random() * 5) + 0)
useEffect(() => console.log(points))
return (
<div onClick={() => UpdatePoints(random())}> ClickMe </div>
);
It looks like it should work for the most part. I'd say these two functions are conflicting, and causing an issue. Are there any errors returned in the console?
const setVote = (value) => setPoint(value)
const setVote => {
const copy = [...points]
copy[selected] += 1
return copy;
}
This last one you may want to rename. Something like setVoteAt which take an index parameter for which vote to set.
const setVoteAt = (index) => {
const copy = [...points]
copy[index] += 1
return copy;
}
setVoteAt(2) // [0, 0, 1, 0, 0, 0]
How to correctly initialize a state array React JS
Use useEffect to initialize it properly. This is similar to componentDidMount.
const [points, setPoints] = useState([])
useEffect(() =>
{
const initialValue = new Array(6).fill(0);
setPoints(initialValue);
}
, []) // initialize only once (on mount) and not on every render
Is it always initializing or is the copy array not retuned. What is the correct way to do this?
setVote() is not doing things correctly. You can update points every time selected is changed by:
useEffect(() =>
{
setPoints(points => {
points[selected] += 1
return points
})
}
, [selected] // only run this effect every time selected changed
)
setNextItem(2) // this will update selected then run the effect on top
// and also increment points[selected]