Avoiding all items rerendering when updating some items in list - reactjs

really need you help here
I'm loosing a lot of time trying to optimize my app, I think there's something I'm not getting about React
Here's a typical case : I got a form with some interconnected inputs (Changing an input can disabled/enabled anothers)
Something looking like my code :
const Form = (form)=>{
const [inputs, setInputs] = useState(form.inputs)
const updateInputs = (updates)=>{
/**
* Changing some inputs
*/
}
return inputs.map(input=>
<InputComponent {...input} key={input.id} onChange={updateInputs}></InputComponent>
)
}
My problem is when input is changed, all inputs are rerendering.
Memo/PureComponent is useless here because of the updateInputs function
Can't use "useCallback" hook on "updateInputs" because it's caching "inputs" state, which is updated every change
And yes I use React profiler.
I got the same issue with checkbox list & radio list.
Everytime I got a list with a state shared between items, I got this issue.

Thanks to #HaveSpacesuit and this article (not the whole article, just the step #4)
My issue was : i couldn't put useCallback on "updateInputs" because I was making something like :
const updateInputs = useCallback((updates)=>{
const localInputs = [...inputs]
/* update localInputs */
setInputs(localInputs)
}, [])
But doing this would memoize "inputs" state without refreshing it.
Setting "inputs" in the useCallback parameters would create a new function reference every update, which makes useCallback useless.
The solution is :
const updateInputs = useCallback((updates)=>{
setInputs((oldInputs)=>{
const localInputs = [...oldInputs]
/*update localInputs*/
return localInputs
}
}, [])
Like this, the function will update fresh "inputs", and the function will keep his reference between rerenders thanks to useCallback.
I went from ~200ms to ~25ms on every user interaction

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 !

How do I make a list updates only on the second render?

I have a state list that is called 'journal' and I want to add a state object that is called 'record' to the list after the user enters the data and set the state of the record.
Here's my states:
const [journal, setJournal] = useState([]);
const [record, setRecord] = useState({});
And here's the method that takes the data from user to set the record:
function AddRecord(debitAccount, debitValue, creditAccount, creditValue, description){
setRecord({date: new Date().getDate().toString(), debit: {[debitAccount]: debitValue},
credit: {[creditAccount]: creditValue}, description, id: new Date().getTime().toString()});
}
I'm using a useEffect to update the journal every time the record changes like this:
useEffect(()=>{
setJournal([...journal, record])
}, [record])
But it adds an empty object at the beginning of the array.
Can someone please tell me how to fix this, I'm still trying to figure my way around states in react, and they're just getting complicated
This is a misuse of useEffect. Effects should be used to react to, and tie together, things which happen outside the business logic of the component (i.e. prop changes, multiple concurrent fetch calls), or resubscribe listeners which are dependent on state values. Just move all the relevant code into AddRecord:
function AddRecord(debitAccount, debitValue, creditAccount, creditValue, description){
const newRecord = {
date: new Date().getDate().toString(),
debit: {
[debitAccount]: debitValue
},
credit: {
[creditAccount]: creditValue
},
description,
id: new Date().getTime().toString()
};
setRecord(newRecord);
setJournal([...journal, newRecord]);
}
useEffect will be called on component initialization try to use some other way maybe useCallback function
useEffect runs on mount, and each time record changes, that's why it runs when record is empty. Just add a condition:
Also do not use journal directly, it's not guarantee that it has the expected value. Use set state callback instead:
useEffect(() => {
if (record && record.id) {
setJournal(journals => ([...journals, record]))
}
}, [record])

React JS component declare a variable required to assign value on useEffect to use across multiple functions

In a React Component, Need to declare variable to access across multiple functions. Two approaches tried to achieve this - useState (less good because of rendering) or let/var. Example:
const [userName, setuserName] = useState("");
let root = "";
problem (part 1): on useEffect after setting setuserName hook I can not access userName immediately. but after modifying root I can use the variable immediately. Example:
useEffect(() => {
//get name from firebase doc ref...
let name = user.claims.userName; //"AName"
setuserName(name);
root = name ;
console.log("userName: " + userName+ " , " + root); //here userName is empty but root has the name.
return () => {
// db.ref("").off("value", listener);
};
}, []);
problem (part 2): if both used in a function declared in the component problem part 1 reverses, I mean, the userName (hook) will have name in it but the root variable will be empty. Example:
async function handleSubmit(e) {
// e.preventDefault();
console.log("root = " + root + " , userName : " + userName );//here root is empty but userName has the name.
return; //or call methods to firebase ref.
}
Tested with assigning variables and state with string literals from code within (Eg: root = "testName"). Just need a temporary variable to call firebase functions. Why this behaviour happens and what approach should I use here?
Update:
Re rendering of component will reset the variable, using state hook is kinda overkill but only option.
Now to set the value of state (userName), useEffect is used in the first place. It also calls and fetches other variables from firebase based on the state (userName). If you dont have that reference nothing else works. You dont have that reference because useState will have it on next render, so not usable right here. You can store userName on a variable. Again render will reset that variable, so wont work with variable either. Hope this make sense.
Now I have solved this, thought it would be interesting to put on SO.
first issue is related to the fact the setState doesn't update state synchronous, which is a common doubt, hence console.log will not print next state. And you can't await on some state fwiw, since it's not an actually promise. If you need to perform some action based on a state change you should create another use useEffect with that state as dependency:
useEffect(() => {
//get name from firebase doc ref...
let name = user.claims.userName; //"AName"
setuserName(name);
return () => {
// db.ref("").off("value", listener);
};
}, []);
useEffect(() => {
// another useEffect based on userName as dependency
//do something on userName update...
console.log(userName);
}, [userName]);
second issue is that on rerenders only the state value is reflected correctly. Once setuserName is resolved and your component is rerendered, any declared variables like root will be recreated based on its original logic (unless you memoize it with useMemo to avoid some expensive calculation).
you should better use state across places/functions to control the logic of your component, not variables declared at your component body which would lead to unexpected behaviors.

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

Input element's value in react component is not getting re-rendered when the state changes

My state object is a Map
const [voucherSet, setVoucherSet] = useState(initialVoucherSet);
initialVoucherSet is map I create at the beginning of the stateless component function.
const initialVoucherSet = new Map();
activeVoucher.voucherDenominations.forEach(voucherDemonination=> {
initialVoucherSet.set(voucherDemonination, 0);
});
const [voucherSet, setVoucherSet] = useState(initialVoucherSet);
activeVoucher.voucherDenominations an array of numbers.
I have a input which triggers a function on onChange.
const handleChange = (e)=>{
const voucherDemonination = parseInt(e.target.id);
const voucherQuantity = parseInt(e.target.value);
if (voucherQuantity >= 0) { setVoucherSet(voucherSet.set(voucherDemonination, voucherQuantity)); }
}
The state object voucherSet is getting updated, but my input's value is not getting re-rendered.
Below is the input element:
<CounterInput type='number' id={voucherDemonination} onChange={handleChange} value={voucherSet.get(voucherDemonination)} />
What I already tried
I thought that it might be because I was not setting a different object to the voucherSet state variable. So I tried something a bit hacky...
const handleChange = (e)=>{
const voucherDemonination = parseInt(e.target.id);
const voucherQuantity = parseInt(e.target.value);
if (voucherQuantity >= 0) {
const tempVoucherSet = voucherSet;
tempVoucherSet.set(voucherDemonination, voucherQuantity);
setVoucherSet(tempVoucherSet);
}
}
but it still didn't work.
Where am I wrong?
Much Thanks in advance! :)
So what is happening is that the Map itself is not changing (eg. every time you update the Map, you still have a reference to the same exact Map in memory), so react is not rerendering.
This falls under the whole "immutable" thing with react. Any time a state change happens, a new object or array ow whatever should be created so that react and easily detect that something changed and thus rerender. This makes it so react doesn't have to iterate over every key in your object/array to see if anything changed (which would kill your performance).
Try this in the code which updates your map:
tempVoucherSet.set(voucherDemonination, voucherQuantity);
setVoucherSet(new Map(tempVoucherSet)); // -> notice the new Map()
This is analogous to other code you may have seen with react and state changes where new objects/arrays are created any time a new property/item is added:
setState({ ...oldState, newProperty: 'newValue' })
setState([ ...oldArray, newItem ]);
I had the same issue in the past. Set your state this way:
setVoucherSet(new Map(voucherSet.set(voucherDemonination, voucherQuantity)));
That will cause a re-render.

Resources