How to design my React project better without using shouldComponentUpdate - reactjs

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.

Related

React losing state when component re-renders

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 !

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 */}
);
}

Using setTimeout to avoid re-renders in React

There is a pattern that I use often that I feel must be an anti-pattern but I don't know a better alternative.
Occasionally my components may receive one or more events that mean that a re-render is necessary. Sometimes it is the case that I do not know how many times the event handlers will be called. To prevent multiple re-renders as a result of many calls to the handlers I do something like this:
_myEventHandler() { // may be called multiple times between renders
if (!this._updateQueued) {
this._updateQueued = true;
this._updateTimer = setTimeout(() => {
this._updateQueued = false;
this.forceUpdate();
}, 0);
}
}
The problem here is I feel it can't be performant due to the latency between code stopping and the event loop starting.
A real world example of this is when I am using react-visibility-sensor and I have multiple elements change their visibility at once, I don't want to re-render for each element, instead I want just one re-render once all the updates have been received.
Is there another better way to deal with multiple calls?
BTW: if you are going to use the above hack don't forget to call clearTimout(this._updateQueued) in your componentWillUnmount
A debounce will reduce the number of times a certain piece of code is run, regardless of how often it is called. Here is a rather simple implementation.
const debounce = (callable, time) => {
let timeout;
return function() {
const functionCall = () => callable.apply(this, arguments);
clearTimeout(timeout);
timeout = setTimeout(functionCall, time);
};
};
And this is how to use it.
const debouncedIteration = debouce(() => {
console.log("Iteration"); // This will be called once every 1000 milliseconds.
}, 1000);
while (true) {
debouncedIteration();
}
You can avoid re-renders using this lifecycle method shouldComponentUpdate (as also mentioned by #fungusanthrax). Keep this inside your react component :
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
using isEqual from lodash here, make sure to include it.
This will only re-render your component when there's a change in props or state value.
To install lodash:
npm install -S lodash
and import isEqual in your component file :
import isEqual from 'lodash/isEqual';

Why is using refs slowing down my React application?

I need a few refs of elements to be stored in Redux so that the elements can be focused on.
I have this dropdown:
<BasicSelect
selectRef={(e) => this.storeRef('make', e) } ... />
And here is storeRef:
storeRef(list, ref)
{
if(this.state.refsStored[list]) {
return;
} else {
this.props.storeSelectRef(list, ref);
var refState = Object.assign({}, this.state.refsStored);
refState[list] = true;
this.setState({refsStored: refState});
}
}
From this, it should store the ref once, then simply return after comparing.
However, every time a option in the dropdown of <BasicSelect> is clicked on, the application hangs for a <1s (noticable), and then continues on.
If I change storeRef to the following (obviously the intended result doesn't work):
storeRef(list, ref) {
return;
}
The dropdown selection is super fast, and all is good. So how come this comparison if(this.state.refsStored[list]) is significantly slow?
Don't put refs into component state. Component state should only be used for things that you will need when rendering, and should cause a re-render when you update them. Save refs directly onto the component instance:
<BasicSelect selectRef={selectInstance => this.selectInstance = selectInstance} />
I'm also kind of confused what selectRef is as a prop, to be honest, but I assume that the <BasicSelect> component forwards that ref on to an underlying <select>tag?
Anyway, the key point is that you are causing unnecessary re-renders every time because you're calling setState(). Don't do that.

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.

Resources