Reactjs: Unknown why function re-run second time - reactjs

I started learning react, yesterday I ran into this issue, somebody please explain me.
When I click button "add to wishlist" the piece of code below run 1st time, set product property "inWishList": true, but unknown why it rerun and set it back to "false" value.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList;
console.log(latest_Products);
return latest_Products;
});
};
_ The piece of code below works perfect, run only 1 time, however, i don't understand the difference between 2 codes
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList];
updatedProducts[prodIndex] = {
...currentProdList[prodIndex],
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};

In the first snippet you are mutating the state object when toggling the inWishList property.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList; // <-- state mutation
console.log(latest_Products);
return latest_Products;
});
};
The second snippet you not only shallow copy the previous state, but you also shallow copy the element you are updating the inWishList property of.
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList]; // <-- shallow copy state
updatedProducts[prodIndex] = {
...currentProdList[prodIndex], // <-- shallow copy element
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};
Now the reason these two code snippets function differently is likely due to rendering your app into a StrictMode component.
StrictMode
Specifically in reference to detecting unexpected side effects.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
When you pass a function to setProducts react actually invokes this twice. This is exposing the mutation in the first example while the second example basically runs the same update twice from the unmutated state, so the result is what you expect.

Related

How to re-render a component when a non state object is updated

I have an object which value updates and i would like to know if there is a way to re-render the component when my object value is updated.
I can't create a state object because the state won't be updated whenever the object is.
Using a ref is not a good idea(i think) since it does not cause a re-render when updated.
The said object is an instance of https://docs.kuzzle.io/sdk/js/7/core-classes/observer/introduction/
The observer class doesn't seem to play well with your use case since it's just sugar syntax to manage the updates with mutable objects. The documentation already has a section for React, and I suggest following that approach instead and using the SDK directly to retrieve the document by observing it.
You can implement this hook-observer pattern
import React, { useCallback, useEffect, useState } from "react";
import kuzzle from "./services/kuzzle";
const YourComponent = () => {
const [doc, setDoc] = useState({});
const initialize = useCallback(async () => {
await kuzzle.connect();
await kuzzle.realtime.subscribe(
"index",
"collection",
{ ids: ["document-id"] },
(notification) => {
if (notification.type !== "document" && notification.event !== "write")
return;
// getDocFromNotification will have logic to retrieve the doc from response
setDoc(getDocFromNotification(notification));
}
);
}, []);
useEffect(() => {
initialize();
return () => {
// clean up
if (kuzzle.connected) kuzzle.disconnect();
};
}, []);
return <div>{JSON.stringify(doc)}</div>;
};
useSyncExternalStore, a new React library hook, is what I believe to be the best choice.
StackBlitz TypeScript example
In your case, a simple store for "non state object" is made:
function createStore(initialState) {
const callbacks = new Set();
let state = initialState;
// subscribe
const subscribe = (cb) => {
callbacks.add(cb);
return () => callbacks.delete(cb);
};
// getSnapshot
const getSnapshot = () => state;
// setState
const setState = (fn) => {
state = fn(state);
callbacks.forEach((cb) => cb());
};
return { subscribe, getSnapshot, setState };
}
const store = createStore(initialPostData);
useSyncExternalStore handles the job when the update of "non state object" is performed:
const title = React.useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().title
);
In the example updatePostDataStore function get fake json data from JSONPlaceholder:
async function updatePostDataStore(store) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${Math.floor(Math.random()*100)+1}`)
const postData = await response.json()
store.setState((prev)=>({...prev,...postData}));
};
My answer assumes that the object cannot for some reason be in React as state (too big, too slow, too whatever). In most cases that's probably a wrong assumption, but it can happen.
I can't create a state object because the state won't be updated whenever the object is
I assume you mean you can't put that object in a React state. We could however put something else in state whenever we want an update. It's the easiest way to trigger a render in React.
Write a function instead of accessing the object directly. That way you can intercept every call that modifies the object. If you can reliably run an observer function when the object changes, that would work too.
Whatever you do, you can't get around calling a function that does something like useState to trigger a render. And you'll have to call it in some way every time you're modifying the object.
const myObject = {};
let i = 0;
let updater = null;
function setMyObject(key, value) {
myObject[key] = value;
i++;
if (updater !== null) {
updater(i);
}
};
Change your code to access the object only with setMyObject(key, value).
You could then put that in a hook. For simplicity I'll assume there's just 1 such object ever on the page.
function useCustomUpdater() {
const [, setState] = useState(0);
useEffect(()=>{
updater = setState;
return () => {
updater = null;
}
}, [setState]);
}
function MyComponent() {
useCustomUpdater();
return <div>I re-render when that object changes</div>;
}
Similarly, as long as you have control over the code that interacts with this object, you could wrap every such call with a function that also schedules an update.
Then, as long as your code properly calls the function, your component will get re-rendered. The only additional state is a single integer.
The question currently lacks too much detail to give a good assessment whether my suggested approach makes sense. But it seems like a very simple way to achieve what you describe.
It would be interesting to get more information about what kind of object it is, how frequently it's updated, and in which scope it lives.

react-hooks/exhaustive-deps and empty dependency lists for "on mount" [duplicate]

This is a React style question.
TL;DR Take the set function from React's useState. If that function "changed" every render, what's the best way to use it in a useEffect, with the Effect running only one time?
Explanation We have a useEffect that needs to run once (it fetches Firebase data) and then set that data in application state.
Here is a simplified example. We're using little-state-machine, and updateProfileData is an action to update the "profile" section of our JSON state.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
}, []);
return (
<p>Hello, world!</p>
);
}
However, ESLint doesn't like this:
React Hook useEffect has a missing dependency: 'actions'. Either include it or remove the dependency array
Which makes sense. The issue is this: actions changes every render -- and updating state causes a rerender. An infinite loop.
Dereferencing updateProfileData doesn't work either.
Is it good practice to use something like this: a single-run useEffect?
Concept code that may / may not work:
const useSingleEffect = (fxn, dependencies) => {
const [ hasRun, setHasRun ] = useState(false);
useEffect(() => {
if(!hasRun) {
fxn();
setHasRun(true);
}
}, [...dependencies, hasRun]);
};
// then, in a component:
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
useSingleEffect(async () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
}, [actions]);
return (
<p>Hello, world!</p>
);
}
But at that point, why even care about the dependency array? The initial code shown works and makes sense (closures guarantee the correct variables / functions), ESLint just recommends not to do it.
It's like if the second return value of React useState changed every render:
const [ foo, setFoo ] = useState(null);
// ^ this one
If that changed every render, how do we run an Effect with it once?
Ignore the eslint rule for line
If you truly want the effect to run only once exactly when the component mounts then you are correct to use an empty dependency array. You can disable the eslint rule for that line to ignore it.
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
get_data_async();
// NOTE: Run effect once on component mount, please
// recheck dependencies if effect is updated.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Note: If you later update the effect and it needs to run after other dependencies then this disabled comment can potentially mask future bugs, so I suggest leaving a rather overt comment as for the reason to override the established linting rule.
Custom hook logic
Alternatively you can use a react ref to signify the initial render. This is preferable to using some state to hold the value as updating it would trigger unnecessary render.
const MyComponent = () => {
const { actions, state } = useStateMachine({updateProfileData, updateLoginData});
const initialRenderRef = useRef(true);
useEffect(() => {
const get_data_async = () => {
const response = await get_firebase_data();
actions.updateProfileData( {user: response.user} );
};
if (initialRenderRef.current) {
initialRenderRef.current = false;
get_data_async();
}
}, [actions]); // <-- and any other dependencies the linter complains about
return (
<p>Hello, world!</p>
);
}
And yes, absolutely you can factor this "single-run logic" into a custom hook if it is a pattern you find used over and over in your codebase.

Having to dynamically change the state : how to avoid an infinite-loop?

I have a React component which receives data from a parent.
Now, this data is dynamic and I do not know beforehand what the properties are called exactly.
I need to render them in a certain fashion and that all works perfectly fine.
Now though, these dynamic objects have a property which is a number, which has to be displayed in my component.
To do so, I thought while iterating over the data, I will add the values to the sum, which is to be displayed. Whenever one of the data object changes, the sum will change, too (since I am using useState and React will detect that change.
But that exactly is the problem I don't know how to solve.
It is obvious that right now my code generates an infinite-loop:
The component is created and rendered for the first time.
During this process, setSum() is called, and therefore changing the state.
React detects that and orders a re-rendering.
So how do I fix this? I feel like I am missing something quite obvious here, but I am too invested to see it.
I have tried to boil down my code to the most easy to read code snippet which focuses on the problem only. Any suggestions to improve the readabilty are welcome!
const ComponentA = (data) => {
const [sum, setSum] = React.useState(0);
const renderData = (dataToRender) => {
//Here lies the problem already
setSum(0)
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<JSX Item>Content</JSX Item>);
//and some more stuff, not relevant
// will not get this far
const newSum = sum+objData.propertyAmount;
setSum(newSum);
});
return result;
};
return(
//...someJSXElements
{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
);
}
The reason it's re-rendering infinitely is because you are setting the state every time the component is rendered, which subsequently triggers another re-render. What you need to do is separate your display code from your state-setting code. I initially thought that useEffect would be a good solution (you can see the edit history for my original answer), however from the React docs:
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. (React docs)
So you could therefore try something like this:
const reducer = (sum, action) => {
switch (action.type) {
case "increment":
return sum + action.propertyAmount;
default:
throw new Error();
}
};
const initialSum = 0;
const ComponentA = (data) => {
const [sum, sumReducer] = React.useReducer(reducer, initialSum);
React.useEffect(() => {
data.relevantObjectArray.forEach((objData, index) => {
sumReducer({ type: "increment", propertyAmount: objData.propertyAmount });
});
}, [data.relevantObjectArray]);
const renderData = (dataToRender) => {
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<div>Content</div>);
});
return result;
};
return (
<div>
{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
</div>
);
};
Example on codesandbox.io.
const ComponentA = (data) => {
const [sum, setSum] = React.useState(0);
const renderData = (dataToRender) => {
//Here lies the problem already
setSum(0)
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<JSX Item>Content</JSX Item>);
//and some more stuff, not relevant
// will not get this far
const newSum = sum+objData.propertyAmount;
setSum(newSum);
});
return result;
};
React.useEffect(() => {
renderData();
return () => {
console.log('UseEffect cleanup')});
}, [data);
return(
//...someJSXElements
//The line below is causing the continuos re-render because you keep calling the function (renderData)
//{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
);
}

React how to use state in passed function

I am using React context to pass down state. When my state changes, it also changes in the child Component (console logs shows new value), but when this state is used in a function, it doesnt update there (console.log shows old value).
Do I need to rerender the function? How?
const {user, userInfo, ref} = useSession(); <-- wrapper for useContext
console.log(userInfo); <--- correct, updated value
const haalDataOp = async () => {
console.log(userInfo.enelogic); <--- old value displaying
...
}
I am using the function haalDataOp from a button (onClick)
As someone already mentioned, I could use useRef, but I dont understand why. Why does this simple example work (extracted from https://dev.to/anpos231/react-hooks-the-closure-hell-71m), and my code doesnt:
const [value, setValue] = useState(1);
const handleClick = useCallback(
() => {
setValue(value + 1)
},
[value],
);
I also tried using useCallback (with userInfo in the dep array) in my example but that doesnt do the trick.
const ... userInfo ... is a constant, so in a Component like following:
console.log('render', userInfo.enelogic) // different value in each render
const haalDataOp = async () => {
console.log('before', userInfo.enelogic) // correct old value
await update()
console.log('after', userInfo.enelogic) // still the same old value
}
return <button onClick={haalDataOp} />
...it would log:
render old
before old
after old
render new
...because the userInfo inside haalDataOp is a closure referencing to the value from the original render. If you need to access a mutable reference that would point to the up-to-date value from a future render instead, you can useRef:
const userInfoRef = useRef()
userInfoRef.current = userInfo
console.log('render', userInfo.enelogic) // different value in each render
const haalDataOp = async () => {
console.log('before', userInfoRef.current.enelogic) // old value
await update()
console.log('after', userInfoRef.current.enelogic) // should be new value
}
return <button onClick={haalDataOp} />
However, there might be a race condition and/or the execution of the 'after' code happens deterministically BEFORE the next render, in which case you will need to use some other trick...
I suspect that the const {ref} = useSession() is needed for exactly this situation, so please read the documentation.

Using React Hooks, why are my event handlers firing with the incorrect state?

I'm trying to create a copy of this spinning div example using react hooks. https://codesandbox.io/s/XDjY28XoV
Here's my code so far
import React, { useState, useEffect, useCallback } from 'react';
const App = () => {
const [box, setBox] = useState(null);
const [isActive, setIsActive] = useState(false);
const [angle, setAngle] = useState(0);
const [startAngle, setStartAngle] = useState(0);
const [currentAngle, setCurrentAngle] = useState(0);
const [boxCenterPoint, setBoxCenterPoint] = useState({});
const setBoxCallback = useCallback(node => {
if (node !== null) {
setBox(node)
}
}, [])
// to avoid unwanted behaviour, deselect all text
const deselectAll = () => {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
}
// method to get the positionof the pointer event relative to the center of the box
const getPositionFromCenter = e => {
const fromBoxCenter = {
x: e.clientX - boxCenterPoint.x,
y: -(e.clientY - boxCenterPoint.y)
};
return fromBoxCenter;
}
const mouseDownHandler = e => {
e.stopPropagation();
const fromBoxCenter = getPositionFromCenter(e);
const newStartAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
setStartAngle(newStartAngle);
setIsActive(true);
}
const mouseUpHandler = e => {
deselectAll();
e.stopPropagation();
if (isActive) {
const newCurrentAngle = currentAngle + (angle - startAngle);
setIsActive(false);
setCurrentAngle(newCurrentAngle);
}
}
const mouseMoveHandler = e => {
if (isActive) {
const fromBoxCenter = getPositionFromCenter(e);
const newAngle =
90 - Math.atan2(fromBoxCenter.y, fromBoxCenter.x) * (180 / Math.PI);
box.style.transform =
"rotate(" +
(currentAngle + (newAngle - (startAngle ? startAngle : 0))) +
"deg)";
setAngle(newAngle)
}
}
useEffect(() => {
if (box) {
const boxPosition = box.getBoundingClientRect();
// get the current center point
const boxCenterX = boxPosition.left + boxPosition.width / 2;
const boxCenterY = boxPosition.top + boxPosition.height / 2;
// update the state
setBoxCenterPoint({ x: boxCenterX, y: boxCenterY });
}
// in case the event ends outside the box
window.onmouseup = mouseUpHandler;
window.onmousemove = mouseMoveHandler;
}, [ box ])
return (
<div className="box-container">
<div
className="box"
onMouseDown={mouseDownHandler}
onMouseUp={mouseUpHandler}
ref={setBoxCallback}
>
Rotate
</div>
</div>
);
}
export default App;
Currently mouseMoveHandler is called with a state of isActive = false even though the state is actually true. How can I get this event handler to fire with the correct state?
Also, the console is logging the warning:
React Hook useEffect has missing dependencies: 'mouseMoveHandler' and 'mouseUpHandler'. Either include them or remove the dependency array react-hooks/exhaustive-deps
Why do I have to include component methods in the useEffect dependency array? I've never had to do this for other simpler component using React Hooks.
Thank you
The Problem
Why is isActive false?
const mouseMoveHandler = e => {
if(isActive) {
// ...
}
};
(Note for convenience I'm only talking about mouseMoveHandler, but everything here applies to mouseUpHandler as well)
When the above code runs, a function instance is created, which pulls in the isActive variable via function closure. That variable is a constant, so if isActive is false when the function is defined, then it's always going to be false as long that function instance exists.
useEffect also takes a function, and that function has a constant reference to your moveMouseHandler function instance - so as long as that useEffect callback exists, it references a copy of moveMouseHandler where isActive is false.
When isActive changes, the component rerenders, and a new instance of moveMouseHandler will be created in which isActive is true. However, useEffect only reruns its function if the dependencies have changed - in this case, the dependencies ([box]) have not changed, so the useEffect does not re-run and the version of moveMouseHandler where isActive is false is still attached to the window, regardless of the current state.
This is why the "exhaustive-deps" hook is warning you about useEffect - some of its dependencies can change, without causing the hook to rerun and update those dependencies.
Fixing it
Since the hook indirectly depends on isActive, you could fix this by adding isActive to the deps array for useEffect:
// Works, but not the best solution
useEffect(() => {
//...
}, [box, isActive])
However, this isn't very clean: if you change mouseMoveHandler so that it depends on more state, you'll have the same bug, unless you remember to come and add it to the deps array as well. (Also the linter won't like this)
The useEffect function indirectly depends on isActive because it directly depends on mouseMoveHandler; so instead you can add that to the dependencies:
useEffect(() => {
//...
}, [box, mouseMoveHandler])
With this change, the useEffect will re-run with new versions of mouseMoveHandler which means it'll respect isActive. However it's going to run too often - it'll run every time mouseMoveHandler becomes a new function instance... which is every single render, since a new function is created every render.
We don't really need to create a new function every render, only when isActive has changed: React provides the useCallback hook for that use-case. You can define your mouseMoveHandler as
const mouseMoveHandler = useCallback(e => {
if(isActive) {
// ...
}
}, [isActive])
and now a new function instance is only created when isActive changes, which will then trigger useEffect to run at the appropriate moment, and you can change the definition of mouseMoveHandler (e.g. adding more state) without breaking your useEffect hook.
This likely still introduces a problem with your useEffect hook: it's going to rerun every time isActive changes, which means it'll set the box center point every time isActive changes, which is probably unwanted. You should split your effect into two separate effects to avoid this issue:
useEffect(() => {
// update box center
}, [box])
useEffect(() => {
// expose window methods
}, [mouseMoveHandler, mouseUpHandler]);
End Result
Ultimately your code should look like this:
const mouseMoveHandler = useCallback(e => {
/* ... */
}, [isActive]);
const mouseUpHandler = useCallback(e => {
/* ... */
}, [isActive]);
useEffect(() => {
/* update box center */
}, [box]);
useEffect(() => {
/* expose callback methods */
}, [mouseUpHandler, mouseMoveHandler])
More info:
Dan Abramov, one of the React authors goes into a lot more detail in his Complete Guide to useEffect blogpost.
React Hooks useState+useEffect+event gives stale state.
seems you are having similar problems. basic issue is that "it gets its value from the closure where it was defined"
try that Solution 2 "Use a ref". in your scenario
Add below useRef, and useEffect
let refIsActive = useRef(isActive);
useEffect(() => {
refIsActive.current = isActive;
});
then inside mouseMoveHandler , use that ref
const mouseMoveHandler = (e) => {
console.log('isActive',refIsActive.current);
if (refIsActive.current) {
I have created an NPM module to solve it https://www.npmjs.com/package/react-usestateref and it seems that it may help and answer your question how to fire the current state.
It's a combination of useState and useRef, and let you get the last value like a ref
Example of use:
const [isActive, setIsActive,isActiveRef] = useStateRef(false);
console.log(isActiveRef.current)
More info:
https://www.npmjs.com/package/react-usestateref

Resources