useContext value changes on rerenders - reactjs

So I have a component that looks something like
const App = () => {
const someContextValue = useSomeContext(); //custom hook that calls useContext
useEffect(() => {
someContextValue()
}, [someContextValue]);
return <div />
}
Whenever the component rerenders, the useEffect is triggered even though someContextValue hasn't really changed.
I got around this by using useMemo like
const someContextValue = useMemo(useSomeContext, [useSomeContext])
Now someContextValue doesn't change on rerenders. But I feel like this isn't quite right. What would be the right way to do this?

If you're returning an object {} or array [] from the context, then the context value HAS changed.
The someContextValue variable is defined inside the component.
The component is a function, and when a function runs, the values that are defined inside it get defined. If for example, your context returns an object containing state values, then that object is a different instance to the one from the previous render, and your useEffect runs because of that.
useEffect will compare reference-types by their instance and not the values inside them.
When you call useSomeContext(), you're creating a new object, and this happens every time the component renders.
This is not the same as a state value from useState, where the state value does not get redefined. The useState hook maintains the same instance of the value, therefore it is not recreated every time, and the useEffect sees that the state value is the same instance.
This is why, when using context, you should destructure the context object and refererence the values inside the object, which are either state values passed from a useState hook inside the context, or a value defined inside the context that does not get redefined when your consuming component re-renders (because the context does not re-render in that case):
const { someStateValue } = useSomeContext()
useEffect(() => {
someStateValue()
}, [someStateValue]);

Related

Why do we need to use setValue in UseState?

So, React has UseState hook, and we should use it like this.
const [value, setValue] = useState(0);
And if we want to change a value, it should be written like this:
const increase = () => {
setValue(value + 1)
}
Why it's not possible to write it like this? Why we need setValue?
const increase = () => {
return value + 1;
}
I understand that it just doesn't work, but I couldn't find an explanation why it is like that.
The useState hook returns a function to update the state. Using setValue(value) method, you can get the previous(or old) state value to update the state.
On the other hand, without useState when you tries to increment the value, the value will not change because the react rendered the component only once and since there is no state change it won't get re-rendered, the value will remain at 0.
You can see here in details : https://dev.to/aasthapandey/why-to-use-usestate-in-react-pkf
Answer is simple...! The state variable is directly linked to rendering your page. The state variable of useState hook is read only for the component to render it in the below return statement which renders the JSX.
The setValue() function not only changes the state value, but will also trigger the react to re-render the DOM which has already being painted on the browser.
Well that's a difference between an ordinary HTML page & a React App. React provides amazing framework to re-render the page automatically after the you alter the state using the setValue() fucntion

Init UseState value from UseContext value

I have context and state variables. My state variable is initialized with my context variable. When I update my context in another component for exemple: changing the player's action (attack to defend), state variable keep the previous value.
const [player,setPlayer] = useContext(PlayerContext);
const [action, setAction] = useState(player.action);
useEffect(() => {
console.log(action); // => attack
console.log(player.action); // => defend
});
This must surely be a rendering problem.
If you want the action state variable to reflect updates to player.action, you need make sure your dependency array (the second parameter to useEffect) includes player.action (so that it only runs when the value changes), and then call setAction explicitly:
useEffect(() => {
if (player.action !== action) {
setAction(player.action);
}
}, [player.action])
The React documentation has an extensive guide on Using the Effect Hook that might be helpful, along with the useEffect hook API reference.
Your context state is changed somewhere after your component is rendered. Therefore the component state is not in sync with the context state anymore. With your browsers debugger you can check this behaviour and find out where it is changed.
As a general rule you should avoid adding a component state, when it shall only mirror a context‘s state, consume the context directly inside your component.

React Re-Render-Loop

I am currently trying to learn about the inner workings of React in context of when a component is re-rendered or especially when (callback-)functions are recreated.
In doing so, I have come across a phenomenon which I just cannot get my head around. It (only) happens when having a state comprising an array. Here is a minimal code that shows the "problem":
import { useEffect, useState } from "react";
export function Child({ value, onChange }) {
const [internalValue, setInternalValue] = useState(value);
// ... stuff interacting with internalValue
useEffect(() => {
onChange(internalValue);
}, [onChange, internalValue]);
return <div>{value}</div>;
}
export default function App() {
const [state, setState] = useState([9.0]);
return <Child value={state[0]} onChange={(v) => setState([v])} />;
}
The example comprises a Parent (App) Component with a state, being an array of a single number, which is given to the Child component. The Child may do some inner workings and set the internal state with setInternalValue, which in turn will trigger the effect. This effect will raise the onChange function, updating a value of the state array of the parent. (Note that this example is minimized to show the effect. The array would have multiple values, where for each a Child component is shown) However this example results in an endless re-rendering of the Child with the following console warning being raised throughout:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Debugging shows, that the re-rendering occurs due to onChange being changed. However, I do not understand this. Why is onChange being changed? Neither internalState nor state is changed anywhere.
There are two workarounds I found:
Remove onChange from the dependencies of the effect in the Child. This "solves" the re-rendering and would be absolutely acceptable for my use case. However, it is bad practice as far as I know, since onChange is used inside the effect. Also, ESLint is indicating this as a warning.
Using a "raw" number in the state, instead of an array. This will also get rid of the re-rendering. However this is only acceptable in this minimal example, as there is only one number used. For a dynamic count of numbers, this workaround is not viable.
useCallback is also not helping and just "bubbling up" the re-recreation of the onChange function.
So my question is: Do React state (comprising arrays) updates are being handled differently and is omitting a dependency valid here? What is the correct way to do this?
Why is onChange being changed?
On every render, you create a new anonymous function (v) => setState([v]).
Since React makes a shallow comparison with the previous props before rendering, it always results in a render, since in Javascript:
const y = () => {}
const x = () => {}
x !== y // always true
// In your case
const onChangeFromPreviousRender = (v) => setState([v])
const onChangeInCurrentRender = (v) => setState([v])
onChangeFromPreviousRender !== onChangeInCurrentRender
What is the correct way to do this?
There are two ways to correct it, since setState is guaranteed to be stable, you can just pass the setter and use your logic in the component itself:
// state[0] is primitive
// setState stable
<Child value={state[0]} onChange={setState} />
useEffect(() => {
// set as array
onChange([internalValue]);
}, [onChange, internalValue]);
Or, Memoizing the function will guarantee the same identity.
const onChange = useCallback(v => setState([v]), []);
Notice that we memoize the function only because of its nontrivial use case (beware of premature optimization).

ReactJS functional component props issue inside useEffect in Hooks

I'm using a functional component as a child with props from parent. In the props i have a value array in which i'm getting as empty([]) inside useEffect and after some time, the same is getting rendered in the UI. I'm using useEffect to call a function and set the state only once as below and i want it to be like that. Here value is the props and checkEmpty is the function i'm checking and setting the state, completed. Is there any way to invoke the function inside useEffect once the value array is filled. But i want the useEffect to be invoked only once and needs to change the state completed with out letting it to an infinite loop.
useEffect(() => {
checkEmpty(value)?setCompleted(false):setCompleted(true)
}, [])
You can pass the value as a dependency of useEffect, and the hook only run when the value is changed.
useEffect(() => {
checkEmpty(value) ? setCompleted(false) : setCompleted(true);
}, [value])

Passed-in prop used to initialize a useState hook gets updated when calling the hook's updater method

Given the following code:
const MyComponent = ({ myObj }) => {
const [myStatefulObj, setMyStatefulObj] = useState(myObj.subObj)
const handleChange = () => {
const updatedObj = { newProperty: Math.random() }
setMyStatefulObj(updatedObj) // myObj is updated too!
}
return <div />
}
I am trying to initialize a new local prop with useState, and hydrating its initial value with a prop that is passed in from a parent component.
When I update my prop using setMyStatefulObj everything works fine, except that it's also updating the prop that's passed into the component.
This is an undesired side effect and one I haven't come across before.
The prop that is getting passed in is from a parent that creates it with a selector from react-redux's useSelector hook.
There are no actions being dispatched or anything, but simply logging the value of the myObj prop shows that it is indeed being updated every time the local setMyStatefulObj method gets called.
The effect you are describing doesn't make sense and I was not able to reproduce it. Maybe you could create a snippet and share it.
I don't think that was caused by the hooks function.
I have created a simple example in CodeSandBox as you said, and it obviously not reproduced. You can edit this sandbox and try to simulate your situation maybe you can find the root cause.
But since you are using redux, I would suggest you to use useSelector to select the state you need inside hooks function, then you would not have this kind of concern, and it will have better performance(if you pass the prop, every time myObj changes the hooks component will re-render).

Resources