React useState is not keeping the changes - reactjs

Current Situation
I am improving performance of the component called ProductMain using React.memo(). The ProductMain is containing ProductComponent that is list. And I am passing every single product information such as name and qty also handleChangeQty method to the ProductComponent as a props. To render ProductComponent separately, I am using React.memo with custom comparator method areEqual
const areEqual = (prevProps, nextProps) => {
const prevLocation = prevProps.product.locations
const nextLocation = nextProps.product.locations
let isEqual = true
if(prevLocation.length !== nextLocation.length) return false
else
for (let i = 0; i < nextLocation.length; i++) {
if (nextLocation[i].adjustedQty !== prevLocation[i].adjustedQty)
isEqual = false
}
return isEqual
}
const ProductComponent: React.FC<Props> = ({ product, productIndex, onQtyChange, onFocusChange }) => {
const handleQtyChange = newValue => {
onQtyChange(locationIndex, location.onHandQty, newValue)
}
return(<React.Fragment>
//some button that can change the qty 'onClick'={handleQtyChange}
</React.Fragment>)
}
export default React.memo(ProductComponent, areEqual)
From the ProductComponent when I change the qty, handleChangeQty function will be working in the ProductMain. ProductMain has a useState hook visibleProduct. Every time handleQtyChange method works, I am updating the visibleProducts using setter function.
Problem is
In the areEqual method, I am comparing previuos props with next props. If these are identical, function returning true and ProductComponent won't be re-rendering. And Its working as expected, re-rendering only changed component. But visibleProducts is not consistently keeping the changes. For example at the very beginning
`visibleProduct = [{productId: 001, qty:0},{productId: 002, qty: 0}]`
After the handleQtyChange works, visibleProduct state changed to
`visibleProduct = [{productId: 001, qty:1},{productId: 002, qty: 0}]`
then I again, I changed the
`visibleProduct = [{productId: 002, qty: 1}]`
the previous value set back to initial value like {productId: 001, qty:0},{productId: 002, qty: 1}
It causes the re-rendering in the child component, and not keeping the previous changes.
Hope you understand well.
I tried to explain more detailed. Sorry for my language.

Related

Why React.Memo() keeps rendering my component

I made an example to get to know more deeply the React.Memo().
I tried to pass a string from the parent to the child. then the component wasn't rendred except the first default rerending.
But When I passed an array from the parent to the child the components get re-render.
So why the component keeps re-render when passing arrays and objects and not re-render when passing string.
Is that related to the reference values and primitive values of javascript?
here's the code for what was I trying to do.
the parent:
import Child from "./Child";
export default function App() {
const name = {
firstName: "Amr"
};
return <Child name={name} />;
}
The Child
import React from "react";
const Child = ({ name }) => {
const name1 = "Amr";
const name2 = "Amr";
// console.log(name1 === name2);
const obj1 = {
name: "Mohamed"
};
const obj2 = {
name: "Mohamed"
};
const arr1 = ["Amr"];
const arr2 = ["Amr"];
console.log(arr1 === arr2);
// console.log(obj1 === obj2); => obj1 => refrence number 1
// obj2 => refrence number 2
// 1 === 2;
// false
areEqual(name, {
firstName: "Amr"
});
return <h2>{name.firstName}</h2>;
};
// javascript [] {} => refrence value
// javascript '' or anythinng primtive value;
// obj1 = 1, obj2 = 2;
// obj1 === obj2 result false 1 === 2
function areEqual(prevProps, nextPops) {
if (prevProps === nextPops) {
console.log("equal");
} else {
console.log("not equal");
}
}
export default React.memo(Child, areEqual);
I used a function also areEqual that returns true or false based on if the prevProps and nextProps are equal or not.
This function already returns for me true when I pass a String and false when I pass an array.
So, am I right or there's something missing.
Thanks
UPDATE:
If what I mentioned is right, so the React.memo() will be useless.
Is that Right?
By default React.memo() does a shallow comparison of props and objects of props - and this is why you were experiencing rerenders when props were of type 'object', because reference recreates on each rerender.
React.memo receives two argument(2nd one is optional), and second one is custom equality function which you can use in order to stabilize rerenders when having props as non primitive values. So, for example, you can do something like this: React.memo(SomeComponent, (prevProps, currentProps) => isDeepEqual(prevProps.someObject, currentProps.someObject)).
Meaning that React.memo is not useless, it just uses shallow comparison by default, but also offers you posibillity to provide it with custom equality checker which you might wanna use in your example. Either you shall use deep equality check within React.memo, or you shall stabilize props in parent component by wrapping their value with useMemo.
It depends on their parent components some times it is necessary React.memo
But in your case, as you said, its reference changes every time you define an array or object. I recommend you to use React.useMemo. For example:
const user = React.useMemo(() => {
return {
firstName: "Amr"
}
}, []); // Second parameter works same as useCallback
React.memo wont re-render the component because user props didn't change

How to control when a component renders? React js for adding a comment to a post

https://codesandbox.io/s/busy-paper-w39kvw?file=/src/components/Comments.js:141-200
const initialState = {
comments: [
{
id: 1,
username: "Kevin",
date: "3 hours ago",
text: "Hello",
votes: 12,
upvoted: false,
downvoted: false,
comments: []
}
]
}
In my comments code I have a useSelector
const { comments } = useSelector(state => state.comments)
this renders all the comments everytime a new comment is added
as shown here using react dev tools add on to highlight when a component renders: https://i.gyazo.com/43a93b6d07a5802d91d9f68684e5ded5.mp4
I tried to use React.memo to memoize the comment but im not sure why that didn't work. Is there anyway to stop rendering all the comments when a comment gets added?
When a component is wrapped in React.memo(), React renders the component and memoizes the result. Before the next render, if the new props are the same, React reuses the memoized result skipping the next rendering.
In your code below, you are passing the allComments function to the comment component.
const allComments = (comments, bg) => {
const output = comments.map((comment) => {
return (
<Comment
key={comment.id}
comment={comment}
allComments={allComments} // the function get's passed as new function object on each rerender
bg={bg}
/>
);
});
return output;
};
what is the problem then and why this behavior?
because of the function equality check, functions in javascript are treated as firstclass citizens, in other word, functions are objects.
function factory() {
return (a, b) => a + b;
}
const sum1 = factory();
const sum2 = factory();
sum1(1, 2); // => 3
sum2(1, 2); // => 3
sum1 === sum2; // => false
sum1 === sum1; // => true
The functions sum1 and sum2 share the same code source but they are different function objects. Comparing them sum1 === sum2 evaluates to false. this is how Javascript works.
In your code, a new function object allComments gets created in each render by react, which is eventually gets passed as a new prop to the React.memo(). By default React.memo() does a shallow comparison of props and objects of props. which is why it triggers the a new rerender.
Now we understand deeply what was the problem and what causes this behavior.
The solution is to wrap your allComments with useCallback
What is the purpose of useCallback? it maintain a single function instance between renderings. and thus React.memo() will work.
const allComments = useCallback((comments, bg) => {
const output = comments.map((comment) => {
return (
<Comment
key={comment.id}
comment={comment}
allComments={allComments}
bg={bg}
/>
);
});
return output;
},[]);

Input values not updating with nested state

I've searched around quite a bit for this, but I'm not able to find a React Hooks example that works where the state has nested objects. I've been mostly following this tutorial. I've created an example fraction calculator component, wherein I want to recalculate the result whenever either input field changes:
import React, { useState } from 'react';
const initialState = {
numerator: {
value: 1
},
denominator: {
value: 2
}
};
export const Fraction = () => {
const [elements, setElements] = useState(initialState);
function changeNumerator(val) {
elements.numerator.value = parseInt(val);
setElements(elements);
}
function changeDenominator(val) {
elements.denominator.value = parseInt(val);
setElements(elements);
}
function calcResult(num, denom) {
if (denom === 0) return;
return num / denom;
}
return <div>
<input
value = {elements.numerator.value}
onChange = {e => changeNumerator(e.target.value)}
/>
/
<input
value = {elements.denominator.value}
onChange = {e => changeDenominator(e.target.value)}
/>
=
<b>{calcResult(elements.numerator.value, elements.denominator.value)}</b>
</div>
}
With this approach, the input values don't update when I change them, though the state seems to be changing (according to console logs and the React dev tools).
I thought maybe I was mutating state, so I attempted the update like this, but still no luck:
function changeNumerator(val) {
let newElements = elements;
newElements.numerator.value = parseInt(val);
setElements(newElements);
}
What am I missing? Is it a mistake to compose state in a nested manner like this? Should numerator and denominator be broken apart? In my real-world example, the structure will be more complex; I know I could make this specific example more straightforward by not structuring the state this way, but I'm looking specifically for how to deal with nested objects, unless there's a compelling reason I should not do that.
since the object pointer hasn't changed, React will not trigger another render.
change this:
function changeNumerator(val) {
elements.numerator.value = parseInt(val);
setElements(elements);
}
to this:
function changeNumerator(val) {
setElements({
...elements,
numerator: {
value: parseInt(val, 10),
},
});
}
and do the same thing with the other function.
In this way, you create a new object, and React will recognize the change.

How to get the closest previous different prop in ReactJS

I have a functional component with props:
function MyComponent({propA, propB, propC}) {
...
}
This component is rendered few times with different props values. I need to get the closest previous prop value which is not equal to current value. For example:
First render. propA is 1. previous value must be undefined
Second render. propA is 2. previous value must be 1
Third render. propA is 2. previous value must be 1 (not 2, because
current value is 2)
There is a similar "usePrevious" hook:
export const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
But it returns any previous value even if it has not changed. And it looks like I should modify this hook, but I don't know how
code example: https://codesandbox.io/s/cranky-snow-mkile?file=/src/index.js (props values are logged in console)
Try this:
const usePrevious = newValue => {
const refPrev = useRef()
const refCurr = useRef()
if (newValue !== refCurr.current) {
refPrev.current = refCurr.current;
refCurr.current = newValue;
}
return refPrev.current;
}

useState() bug - state value different from initial value

I have a component that uses useState() to handle the state of its floating label, like this:
const FloatingLabelInput = props => {
const {
value = ''
} = props
const [floatingLabel, toggleFloatingLabel] = useState(value !== '')
I have a series of those components and you'd expect initialFloatingLabel and floatingLabel to always be the same, but they're not for some of them! I can see by logging the values:
const initialFloatingLabel = value !== ''
console.log(initialFloatingLabel) // false
const [floatingLabel, toggleFloatingLabel] = useState(initialFloatingLabel)
console.log(floatingLabel) // true???
And it's a consistent result. How is that possible?
How come value can be different from initialValue in the following example? Is it a sort of race condition?
const [value, setValue] = useState(initialValue)
More details here
UPDATE
This (as suggested) fixes the problem:
useEffect(() => setFloatingLabel(initialFloatingLabel), [initialFloatingLabel])
...but it creates another one: if I focus on a field, type something and then delete it until the value is an empty string, it will "unfloat" the label, like this (the label should be floating):
I didn't intend to update the floatingLabel state according to the input value at all times; the value of initialFloatingLabel was only meant to dictate the initial value of the toggle, and I'd toggle it on handleBlur and handleChange events, like this:
const handleFocus = e => {
toggleFloatingLabel(true)
}
const handleBlur = e => {
if (value === '') {
toggleFloatingLabel(false)
}
}
Is this approach wrong?
UPDATE
I keep finding new solutions to this but there's always a persisting problem and I'd say it's an issue with Formik - it seems to initially render all my input component from its render props function before the values are entirely computed from Formik's initialValues.
For example:
I added another local state which I update on the handleFocus and handleBlur:
const [isFocused, setFocused] = useState(false)
so I can then do this to prevent unfloating the label when the input is empty but focused:
useEffect(() => {
const shouldFloat = value !== '' && !isFocused
setFloatLabel(shouldFloat)
}, [value])
I'd still do this to prevent pre-populated fields from having an animation on the label from non-floating to floating (I'm using react-spring for that):
const [floatLabel, setFloatLabel] = useState(value !== '')
But I'd still get an animation on the label (from "floating" to "non-floating") on those specific fields I pointed out in the beginning of this thread, which aren't pre-populated.
Following the suggestion from the comments, I ditched the floatingLabel local state entirely and just kept the isFocused local state. That's great, I don't really need that, and I can only have this for the label animation:
const animatedProps = useSpring({
transform: isFocused || value !== '' ? 'translate3d(0,-13px,0) scale(0.66)' : 'translate3d(0,0px,0) scale(1)',
config: {
tension: 350,
},
})
The code looks cleaner now but I still have the an animation on the label when there shouldn't be (for those same specific values I mentioned at the start), because value !== '' equals to true for some obscure reason at a first render and then to false again.
Am I doing something wrong with Formik when setting the initial values for the fields?
You have the use useEffect to update your state when initialFloatingLabel change.
const initialFloatingLabel = value !== ''
const [floatingLabel, setFloatingLabel] = useState(initialFloatingLabel)
// calling the callback when initialFloatingLabel change
useEffect(() => setFloatingLabel(initialFloatingLabel), [initialFloatingLabel])
...
Your problem look like prop drilling issue. Perhaps you should store floatingLabel in a context.
// floatingLabelContext.js
import { createContext } from 'react'
export default createContext({})
// top three component
...
import { Provider as FloatingLabelProvider } from '../foo/bar/floatingLabelContext'
const Container = () => {
const [floatingLabel, setFloatingLabel] = useState(false)
return (
<FloatingLabelProvider value={{ setFloatingLabel, floatingLabel }}>
<SomeChild />
</FloatingLabel>
)
}
// FloatingLabelInput.js
import FloatingLabelContext from '../foo/bar/floatingLabelContext'
const FloatingLabelInput = () => {
const { setFloatingLabel, floatingLabel } = useContext(FloatingLabelContext)
...
}
This way you just have to use the context to change or read the floatingLabel value where you want in your components three.

Resources