React losing state when component re-renders - reactjs

I'm building an application that has a quizz section.
It's only meant to display one question at a time, and they're coming from an API as an array of questions.
I'd like to have a mechanism on the main Quizz component that would know which question is currently being displayed and, when there's a correct answer, move on to the next question.
It works fine for the first question, but once I arrive at the second question, React re-renders my component and my state is reset.
function Quizz() {
const [selectedQuestion, setSelectedQuestion] = useState(0);
const handleQuestionAnswer = useCallback((isRejection) => {
setSelectedQuestion(selectedQuestion + 1);
}, [setSelectedQuestion]);
return {QuizzData.questions.map((question, i) => {
if (i === selectedQuestion) {
return (
<Question data={question} clickCallback={handleQuestionAnswer} key={i} />
);
}
}
The Question component passes the callback to a child, which then invokes the function.

handleQuestionAnswer is closing over the initial state value. Use a functional state update to correctly update from previous state instead of whatever is closed over in callback scope.
Example:
const handleQuestionAnswer = useCallback((isRejection) => {
setSelectedQuestion(selectedQuestion => selectedQuestion + 1);
}, [setSelectedQuestion]);
See Functional Updates for more details.
General "Rule of Thumb": If the next React state value depends on the previous state value, i.e. incrementing a count, use a functional state update.

Your handleQuestionAnswer useCallback seems to have the wrong dependencies, you probably meant selectedQuestion instead of setSelectedQuestion.
As you used the wrong dependencies, your handleQuestionAnswer does not 'update' and is only binded to selectedQuestion=0 (as setSelectedQuestion never update)
configured eslint might have help you notice this issue, so it is worth taking time to set it up !

Related

useEffect dependency list and its best practice to include all used variables

According to the react docs at https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect when using the useEffect dependency array, you are supposed to pass in all the values used inside the effect.
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array values change too often.
I don't know how the hook works behind the scenes, so I'm going to guess here.
Since the variables inside the closure might go stale, that would imply that the function is cached somewhere. But why would you cache the function since its not being called unless the dependencies changed and the function needs to be recreated anyways?
I've made a small test component.
function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => {
console.log("b", b);
}, [a]);
return (
<div>
<div>App {a}</div>
<button
onClick={() => {
setA(a + 1);
}}
>
AddA
</button>
<button
onClick={() => {
setB(b + 1);
}}
>
AddB
</button>
</div>
);
}
You can try it out here: https://codesandbox.io/s/react-hooks-playground-forked-m5se8 and it works just fine, with no stale values. Can someone please explain what I'm missing?
Edit:
After feedback that my question is not entirely clear, adding a more specific question:
When after a page load I click on AddB button and then click on AddA button, value displayed in console is 1. According to the docs, I should get a stale value (0). Why is this not the case?
When after a page load I click on AddB button and then click on AddA
button, value displayed in console is 1. According to the docs, I
should get a stale value (0). Why is this not the case?
The reason why you don't see stale value in that case is that when you click AddA a re-render happens. Inside that new render since value of a is different from previous render, the useEffect will be scheduled to run after react updates the UI (however the values it will reference will be from current render - because the function passed to useEffect is re-created each time one of its dependencies change, hence it captures values from that render).
Due to above reasons, that is why you see fresh value of b.
But if you had such code
React.useEffect(() => {
const timer = window.setInterval(() => {
setB(b + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
b would be a stale closure. Since useEffect didn't re-run, b always has value from the render it was created in, that is the first render and above code wouldn't work as expected, which you can check yourself.
I will add this comment by Patrick Roberts from above because it explains well IMHO what react docs may mean when they say "Otherwise, your code will reference stale values from previous renders":
the problem is that you had to click on AddA button in the first
place. Between the time you click AddB and AddA, the effect references
b from a stale closure

How to design my React project better without using shouldComponentUpdate

I am trying to construct 1-minute candlestick.
I have a component that will continuously passing a number (the trade price) to his child component.
This child component will keep update its state: (High, Low, Open, Close) base on the new number he gets from the parent. (e.g. if the number coming in, is higher than the current this.state.high, it will update this.state.high to the new number) After every minute a setInterval function it will take the states and construct a candle and pass it down to its own children.
the state are:
high, low, open, close, newCandle
I got it working by using
shouldComponentUpdate(nextProps:props, nextState:state){
if(this.props !== nextProps)
this.updateStates(nextProps.newTradePrice); //will update the high, low, open, close state
if(JSON.stringify(nextState.nextMinuteCandle) !== JSON.stringify(this.state.nextMinuteCandle) ) //once the one minute interval is up, there will be a function that will auto set the newCandle state to a new Candle base on the current high, low, open, close state
return true;
return false;
}
I read in the document that shouldComponentUpdate should only be used for optimization not to prevent something to reRender. I am using this to prevent reRender and infinite loop.
I've been stuck on this for days, I cant figure out a way to design this better. Any advice on how to design this better?
2nd related question:
In fact I am relying on shouldComponentUpdate for almost ALL my component too. This can't be right. e.g.
I have a CalculateAverageVolume child component, that takes in the this.state.newCandle. And update the volume every time the newCandle changes (i.e. every minute)
constructor(props: props) {
super(props);
this.state = {
[...],
currentAverage: 0,
showVolume: true
};
}
onCloseHandler()
{
this.setState({showVolume: false});
}
updateAvg(newCandleStick: CandleStick){
//do caluation and use this.setState to update the this.state.currentAverage
}
shouldComponentUpdate(nextProps:props, nextState:state)
{
if(JSON.stringify(this.props.candleStick) !== JSON.stringify(nextProps.candleStick) || this.state.showVolume !== nextState.showVolume){
this.updateAvg(nextProps.candleStick);
return true;
}
return false;
}
render() {
return (
<>
{(this.state.showVolume &&
<IndicatorCard
cardHeader="Volume"
currentInfo={this.state.currentAverage.toString()}
onCloseHandler={()=>this.onCloseHandler()}>
</IndicatorCard>
)}
</>
);
}
}
Can someone please teach me how to design this or restructure this? This works perfectly, but doesn't seem like the right way to do it
I would simplify the component like below.
import { useMemo, useState, memo, useCallback } from "react";
function Component({ candleStick }) {
// use props here to calculate average
const updateAverage = () => 0; // use candleStick props to calculate avg here
const [showVolume, setShowVolume] = useState();
// Compute the average from prop when component re-renders
// I would also add useMemo if `updateAverage` is an expensive function
// so that when prop remains same and `showVolume` changes we don't need to calculate it again
const currentAverage = useMemo(updateAverage, [candleStick]);
const onCloseHandler = useCallback(() => setShowVolume(val => !val), []);
return showVolume ? (
<IndicatorCard
cardHeader="Volume"
currentInfo={currentAverage}
onCloseHandler={onCloseHandler}
/>
) : null;
}
// If true is returned, component won't re-render.
// Btw React.memo by default would do shallow comparison
// But if deep comparison function is required, I would use lodash or other utility to do the check instead of JSON.stringify.
const arePropsEqual = (prev, next) =>
isEqual(prev.candleStick, next.candleStick);
export default memo(Component, arePropsEqual);
shouldComponentUpdate is usually reserved for discrete events that you can control. Howevr, it seems like you are dealing with a continuous stream of data.
Two ways to handle it:
Pass down a function reference that handles a stream to the child component and let that handle you state updates in your child component.
Use the context API to inform child component about the changes
Reference implementation :
Upadting State with Context API : https://javascript.plainenglish.io/react-context-api-part-2-updating-state-through-a-consumer-7be723b54d7b
Streams : https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
React with Streams : https://blog.bitsrc.io/how-to-render-streams-with-react-8986ad32fffa
I hit the same spot you're in when starting React. The problem here is that React, at least the basic aspects of it, isn't enough when you're talking about data flow. What you need to look into is a React data management framework, of which Redux is probably the most popular. Go look at Redux and make sure you're looking at the latest documentation based around hooks.
You'll say to yourself "Oh! That makes perfect sense" - I know I did.
Other, similar frameworks are React Query and React's own Context API. The main point I'm trying to make is that you really need data management to do the thing you're looking for.

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

Throttling dispatch in redux producing strange behaviour

I have this class:
export default class Search extends Component {
throttle(fn, threshhold, scope) {
var last,
deferTimer;
return function () {
var context = scope || this;
var now = +new Date,
args = arguments;
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
}
}
render() {
return (
<div>
<input type='text' ref='input' onChange={this.throttle(this.handleSearch,3000,this)} />
</div>
)
}
handleSearch(e) {
let text = this.refs.input.value;
this.someFunc();
//this.props.onSearch(text)
}
someFunc() {
console.log('hi')
}
}
All this code does it log out hi every 3 seconds - the throttle call wrapping the handleSearch method takes care of this
As soon as I uncomment this line:
this.props.onSearch(text)
the throttle methods stops having an effect and the console just logs out hi every time the key is hit without a pause and also the oSearch function is invoked.
This onSearch method is a prop method passed down from the main app:
<Search onSearch={ text => dispatch(search(text)) } />
the redux dispatch fires off a redux search action which looks like so:
export function searchPerformed(search) {
return {
type: SEARCH_PERFORMED
}
}
I have no idea why this is happening - I'm guessing it's something to do with redux because the issue occurs when handleSearch is calling onSearch, which in turn fires a redux dispatch in the parent component.
The problem is that the first time it executes, it goes to the else, which calls the dispatch function. The reducer probably immediately update some state, and causes a rerender; the re-render causes the input to be created again, with a new 'throttle closure' which again has null 'last' and 'deferTimer' -> going to the else every single time, hence updating immediately.
As Mike noted, just not updating the component can you get the right behavior, if the component doesn't need updating.
In my case, I had a component that needed to poll a server for updates every couple of seconds, until some state-derived prop changed value (e.g. 'pending' vs 'complete').
Every time the new data came in, the component re-rendered, and called the action creator again, and throttling the action creator didn't work.
I was able to solve simply by handing the relevant action creator to setInterval on component mount. Yes, it's a side effect happening on render, but it's easy to reason about, and the actual state changes still go through the dispatcher.
If you want to keep it pure, or your use case is more complicated, check out https://github.com/pirosikick/redux-throttle-actions.
Thanks to luanped who helped me realise the issue here. With that understood I was able to find a simple solution. The search component does not need to update as the input is an uncontrolled component. To stop the cyclical issue I was having I've used shouldComponentUpdate to prevent it from ever re-rendering:
constructor() {
super();
this.handleSearch = _.throttle(this.handleSearch,1000);
}
shouldComponentUpdate() {
return false;
}
I also moved the throttle in to the constructor so there can only ever be once instance of the throttle.
I think this is a good solution, however I am only just starting to learn react so if anyone can point out a problem with this approach it would be welcomed.

ReactJS concurrent SetState race condition

I have a component structure like this
<A>
<B>
<C/>
<C/>
</B>
<D>
<E/>
<E/>
</D>
</A>
Idea is that actions on components in block E are processed by a function of component A to state of A and than passed down to B and C as props. I know, that better way was to use Flux, pubsub-js or other Store-message system, but hope if someone can explain why correct to the best of my understanding solution doesn't work.
Calling this function of component A simalteneously from multiple instances of component E leads to race condition with only one change in state (instead of each function call providing a change)
updateState(value,index){
this.setState(update(this.state, {
a: {
[index]: {
b: {
$set: value
}
}
}
})
);
}
Function update here comes from
import update from 'react/lib/update';
Bad solution that goes against ReactJS reccomended practices, but works well:
updateState(value,index){
this.state.a[index].b=value;
this.forceUpdate();
);
}
My question is:
Is it a bug, that multiple simalteneous setState invokes a race condition, or I'm doing something wrong without understnding it?
You probably want to pass a function to setState which should remove such race conditions. Something like:
this.setState(prevState => {
return {
someProp: prevState.someProp + 1,
};
});
Even if two different parts of your application do this at the same time, both functions will still be called and someProp will be incremented twice.
If you were to do this instead: this.setState({someProp: this.state.someProp + 1}) it could only be incremented once because this.state.someProp isn't updated directly after calling setState. But when passing a function to setState you get the previous state as an argument, which lets you avoid data races.
According to React documentation, as mentioned in the first answer, you can pass a function to ensure the atomicity's operation. The thing is that you should not modify the state but return the new value inside the passed function:
setState(function(previousState, currentProps) {
return {myInteger: previousState.myInteger + 1};
});
https://facebook.github.io/react/docs/component-api.html

Resources