React and JS closure issue - javascript

Background of the problem
I have a simple "Ticker" class that can hold a single callback, and execute that callback each ~1s via setInterval The code is as follows.
class Ticker{
listner = null;
constructor(){
setInterval(this.excuteCallbackInterval, 1000)
}
addListner(callback){
this.listner = callback
}
excuteCallbackInterval = () => {
if(this.listner){
this.listner();
}else {
console.log("no listner registered");
}
}
}
I have a React Functional Component in a nother file, that instantiates a Ticker Object, at class level(I.E outside the functional component) but inside the same file. Here's the code for that component. (some parts are removed to make it concise)
.
.
.
const ticker = new Ticker()
// react component.
function TimerDuration({ changingValue }) {
const unsubscribe = useRef(null)
function runEachSecond() {
console.log('inside runEachSecond', changingValue)
}
useEffect(() => {
unsubscribe.current = ticker.addListener(runEachSecond)
// Cleanup
return () => {
try {
unsubscribe.current()
} catch (err) {
// Do nothing
}
}
}, [])
return (
<Text>
{changingValue}
</Text>
)
}
Problem
My problem is that when the timer TimerDuration renders initially changingValue painted on the screen and changingValue inside excuteCallbackInterval is the same,
But when the changingValue prop is updated, that updated value is not reflected inside excuteCallbackInterval but the change is reflected in the value painted on the screen.
Solution.
My solution was (in a way by instinct) was to store the value of changingValue in a ref and update it inside a second useEffect that runs each time. Might not be the ideal one hence this question.
Question
My question is why am I seeing this behavior? runEachSecond is evaluated with each prop update. And it's able to access changingValue because it's in its parent scope. excuteCallbackInterval function within the Ticker also has access to changingValue because of the closure. But why does the update not reflect within excuteCallbackInterval? Also is there a better way to solve this issue?

My question is why am I seeing this behavior? runEachSecond is evaluated with each prop update.
No, it's not updated. In React, all variables stay constant. A new runEachSecond function is created every time the TimerDuration function component is rendered, and that new function would close over the new changingValue parameter.
However, runEachSecond is only used in the effect (in ticker.addListener(runEachSecond)), which runs only once (when the component is mounted). The ticker will store only the first of the many runEachSecond functions, and it will never change (addListener is not called again), and that first function still closes over the first changingValue.
I just want it to be subscribed after mounting, and unsubscribed when unmounting.
In that case, you should store the changingValue in the ref, and then refer to that ref (and its future values) from the ticker listener function. The ref for the unsubscribe function returned by addListener is unnecessary.
function TimerDuration({ changingValue }) {
const value = useRef(null);
value.current = changingValue;
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
useEffect(() => {
const unsubscribe = ticker.addListener(function runEachSecond() {
console.log('inside runEachSecond', value.current);
// ^^^^^^^^^^^^^
});
// Cleanup
return () => {
try {
unsubscribe();
} catch (err) {
// Do nothing
}
}
}, [])
return (
<Text>
{changingValue}
</Text>
)
}

You want to unsubscribe and subscribe again every time changingValue change. Or, said in another way: you want to update the ticker's callback to prevent it from going stale.
useEffect(() => {
function runEachSecond() {
console.log('inside runEachSecond', changingValue)
}
const unsubscribe = ticker.addListener(runEachSecond)
// Cleanup
return () => {
try {
unsubscribe()
} catch (err) {
// Do nothing
}
}
}, [changingValue]) // see changingValue in the deps
Why do you need to do this? Right now, your component does something like this:
mount
runEachSecond is created (instance #1)
subscribe to Ticker, pass runEachSecond #1
changingValue prop get update
runEachSecond is created again (instance #2)
Ticker still hold a reference of runEachSecond #1
After adding changingValue in the deps of the useEffect:
mount
runEachSecond is created (instance #1)
subscribe to Ticker, pass runEachSecond #1
changingValue prop get update
runEachSecond is created again (instance #2)
Ticker unsubscribe from runEachSecond #1
Ticker subscribe again, but to runEachSecond #2

Related

problem handling promise in useState React

After selecting two values ​​from a list, I must make a call to an api that returns a Json with the information
In this way I call the api that responds with the JSON
export default class ProductsService {
async getProducts(channel, category){
const aux = await axios.post('https://appshop.dapducasse.cl/ducasse-api/api/Qrs?channel='+encodeURIComponent(channel)+'&category='+encodeURIComponent(category)).then(data =>data.data).catch(e => [])
return aux
}
}
In this way I make the call from the react component
const chanelService = new ChanelService();
const categoryService = new CategoryService();
const productsService = new ProductsService();
useEffect(() => {
chanelService.getChanels().then(data => setChanels(data))
categoryService.getCategories().then(data => setCateries(data))
}, []);
const testChargue = async ( ) =>{
console.log(selectedChannel)
console.log(selectedCategory)
const a = await productsService.getProducts(selectedChannel, selectedCategory).then(data => setProducts(data));
console.log(products)
}
When I press the button, the function should be executed where the call is made with the Channel and the selected product.
I don't get the json on the first click of the button, but on the second, I think this happens because the execution ends before setProducts defines the state of products.
I assume products and setProducts refer to local state in your component?
In this case products in the last line will always be the "old" value inside your handler function, because setProducts will not update that variable. The variable will only changed when the component will been re-rendered, but not during the execution of your handler function. I think that's why you see the value when pressing the button twice. On the second click the component has been re-rendered, so the value also in your handler function has been updated.
More on "stale" state in the react docs: https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function

Can i setState of a react functional component from a external gloabal function present inside script tag?

I have an android app which calls the a function present inside the head of my react
page. All i want is to allow the the function to set a state inside the react component
<head>
<script>
webViewAndriodInteraction(parm1)
{
//Here i want this function to change the state of my react functional component
//setSomeState(pram)
}
</script>
</head>;
function LiveScreen(props) {
const [somedata, setSomeState] = useState();
return <h1>someData</h1>;
}
It's probably against some React best practices, but you could do this:
In your component, define an effect that puts your state setter on the window object:
useEffect(() => {
window.setSomeState = setSomeState;
return () => {
window.setSomeState = undefined;
};
}, []);
And in your webViewAndriodInteraction function:
if (window.setSomeState !== undefined) {
// call window.setSomeState here
}
You also need to ensure that your call to window.setSomeState is deferred until the function gets defined. So if you're sure it's going to get defined, you could set a timeout or retry the if check a few times with a given delay.

setState in functional component don't keep value

I have a functional component that keeps a state. This state I try to manipulate using an onClick event in an SVG. The SVG is in another component and has the addAndRemoveSelectedCabin method passed to it via props. I loop through the elements in an useEffect and add an eventListener. This doesn't work. The useEffect with the selectedCabins dependency logs the new number only. It seems the state returns to the initial state after every stateChange.
This is the state and method in the parent component.
const [selectedCabins, setSelectedCabins] = useState([0]);
const addRemoveSelectedCabin = id => {
const newArr = [...selectedCabins, id];
setSelectedCabins(newArr);
}
useEffect(() => {
console.log(selectedCabins);
}, [selectedCabins])
This is how I call the method. [UPDATE]
useEffect(() =>
{
const cabins = document.querySelectorAll(".cabin");
cabins.forEach(cabin =>
{
const id = cabin.getAttributeNS(null, "id").substring(1, 5);
const found = cabinsData.find(el => el.id === id)
if (found && found.status === "available")
{
cabin.classList.add("green")
cabin.addEventListener('click', () => addRemoveSelectedCabin(id));
} else if (found && found.status === "booked")
{
cabin.classList.add("gray")
}
})
}, [])
Console:
[0]
(2) [0, "1105"]
(2) [0, "1101"]
This works if I put the onClick directly in the SVG element. Does anyone know why this is?
<rect
id="C1105"
x="749.4"
y="58.3"
className="cabin"
width="36.4"
height="19.9"
onClick={() => addRemoveSelectedCabin(1105)}
>
<title>1105</title>
</rect>
As I said in my comment, you are binding addRemoveSelectedCabin in the first render. useEffect is only executed once since you pass an empty dependency list. addRemoveSelectedCabin closes over selectedCabins which at that point in time has the value [0].
Why am I seeing stale props or state inside my function? from the React documentation has more information about this.
The solution in your case is simple: Pass a function to the setter to get the "current" state value. Don't reference the state value in the component:
const addRemoveSelectedCabin = id => {
setSelectedCabins(selectedCabins => [...selectedCabins, id]);
}
Having said that, this is still an odd thing to do in React world. You should reevaluate your assumptions that make you think you have to do it that way.
It's not all the elements that should have a click listener.
Depending on how you actually render the elements, that's easy to do. JSX/React is just JavaScript. Whether you have a condition that adds the event handler or not or whether you have a condition that sets onClick or not is basically the same.
But without a more complete example there is not much we can suggest.

React useState hook event handler using initial state

I'm still getting my head around react hooks but struggling to see what I'm doing wrong here. I have a component for resizing panels, onmousedown of an edge I update a value on state then have an event handler for mousemove which uses this value however it doesn't seem to be updating after the value has changed.
Here is my code:
export default memo(() => {
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
console.log(activePoint); // is null but should be 'top|bottom|left|right'
};
const resizerMouseDown = (e, point) => {
setActivePoint(point); // setting state as 'top|bottom|left|right'
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
The problem is with the handleResize function, this should be using the latest version of activePoint which would be a string top|left|bottom|right but instead is null.
How to Fix a Stale useState
Currently, your issue is that you're reading a value from the past. When you define handleResize it belongs to that render, therefore, when you rerender, nothing happens to the event listener so it still reads the old value from its render.
There are a several ways to solve this. First let's look at the most simple solution.
Create your function in scope
Your event listener for the mouse down event passes the point value to your resizerMouseDown function. That value is the same value that you set your activePoint to, so you can move the definition of your handleResize function into resizerMouseDown and console.log(point). Because this solution is so simple, it cannot account for situations where you need to access your state outside of resizerMouseDown in another context.
See the in-scope function solution live on CodeSandbox.
useRef to read a future value
A more versatile solution would be to create a useRef that you update whenever activePoint changes so that you can read the current value from any stale context.
const [activePoint, _setActivePoint] = React.useState(null);
// Create a ref
const activePointRef = React.useRef(activePoint);
// And create our custom function in place of the original setActivePoint
function setActivePoint(point) {
activePointRef.current = point; // Updates the ref
_setActivePoint(point);
}
function handleResize() {
// Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
console.log(activePointRef.current);
}
function resizerMouseDown(event, point) {
/* Truncated */
}
See the useRef solution live on CodeSandbox.
Addendum
It should be noted that these are not the only ways to solve this problem, but these are my preferred methods because the logic is more clear to me despite some of the solutions being longer than other solutions offered. Please use whichever solution you and your team best understand and find to best meet your specific needs; don't forget to document what your code does though.
You have access to current state from setter function, so you could make it:
const handleResize = () => {
setActivePoint(activePoint => {
console.log(activePoint);
return activePoint;
})
};
useRef for the callback
A similar approach to Andria's can be taken by using useRef to update the event listener's callback itself instead of the useState value. This allows you to use many up-to-date useState values inside one callback with only one useRef.
If you create a ref with useRef and update its value to the handleResize callback on every render, the callback stored in the ref will always have access to up-to-date useState values, and the handleResize callback will be accessible to any stale callbacks like event handlers.
function handleResize() {
console.log(activePoint);
}
// Create the ref,
const handleResizeRef = useRef(handleResize);
// and then update it on each re-render.
handleResizeRef.current = handleResize;
// After that, you can access it via handleResizeRef.current like so
window.addEventListener("mousemove", event => handleResizeRef.current());
With this in mind, we can also abstract away the creation and updating of the ref into a custom hook.
Example
See it live on CodeSandbox.
/**
* A custom hook that creates a ref for a function, and updates it on every render.
* The new value is always the same function, but the function's context changes on every render.
*/
function useRefEventListener(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
return fnRef;
}
export default memo(() => {
const [activePoint, setActivePoint] = useState(null);
// We can use the custom hook declared above
const handleResizeRef = useRefEventListener((event) => {
// The context of this function will be up-to-date on every re-render.
console.log(activePoint);
});
function resizerMouseDown(event, point) {
setActivePoint(point);
// Here we can use the handleResizeRef in our event listener.
function handleResize(event) {
handleResizeRef.current(event);
}
window.addEventListener("mousemove", handleResize);
// cleanup removed for clarity
window.addEventListener("mouseup", cleanup);
}
return (
<div className="interfaceResizeHandler">
{resizePoints.map((point) => (
<div
key={point}
className={`interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${point}`}
onMouseDown={(event) => resizerMouseDown(event, point)}
/>
))}
</div>
);
});
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
setActivePoint(currentActivePoint => { // call set method to get the value
console.log(currentActivePoint);
return currentActivePoint; // set the same value, so nothing will change
// or a different value, depends on your use case
});
};
Just small addition to the awe ChrisBrownie55's advice.
A custom hook can be implemented to avoid duplicating this code and use this solution almost the same way as the standard useState:
// useReferredState.js
import React from "react";
export default function useReferredState(initialValue) {
const [state, setState] = React.useState(initialValue);
const reference = React.useRef(state);
const setReferredState = value => {
reference.current = value;
setState(value);
};
return [reference, setReferredState];
}
// SomeComponent.js
import React from "react";
const SomeComponent = () => {
const [someValueRef, setSomeValue] = useReferredState();
// console.log(someValueRef.current);
};
For those using typescript, you can use this function:
export const useReferredState = <T>(
initialValue: T = undefined
): [T, React.MutableRefObject<T>, React.Dispatch<T>] => {
const [state, setState] = useState<T>(initialValue);
const reference = useRef<T>(state);
const setReferredState = (value) => {
reference.current = value;
setState(value);
};
return [state, reference, setReferredState];
};
And call it like that:
const [
recordingState,
recordingStateRef,
setRecordingState,
] = useReferredState<{ test: true }>();
and when you call setRecordingState it will automatically update the ref and the state.
You can make use of the useEffect hook and initialise the event listeners every time activePoint changes. This way you can minimise the use of unnecessary refs in your code.
When you need to add event listener on component mount
Use, useEffect() hook
We need to use the useEffect to set event listener and cleanup the same.
The use effect dependency list need to have the state variables which are being used in event handler. This will make sure handler don't access any stale event.
See the following example. We have a simple count state which gets incremented when we click on given button. Keydown event listener prints the same state value. If we remove the count variable from the dependency list, our event listener will print the old value of state.
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const clickHandler = () => {
console.log({ count });
setCount(c => c + 1);
}
useEffect(() => {
document.addEventListener('keydown', normalFunction);
//Cleanup function of this hook
return () => {
document.removeEventListener('keydown', normalFunction);
}
}, [count])
return (
<div className="App">
Learn
<button onClick={clickHandler}>Click me</button>
<div>{count}</div>
</div>
);
}
export default App;

React hooks vs eventListener

So... I was trying useEffect but I found a strange behavior.
I have a state in a dumb component.
I call useEffect and inside of it I add a new eventListener.
This event listener has to change the state given a condition.
Problem is the state never changes...
Ideas?
const componentToRender=()=>{
const [renderStatus, changeRenderStatus]=useState(false);
const [transitionStatus, changeTransitionStatus]=useState(false);
if(!renderStatus){
useEffect(()=>{
window.addEventListener("transitionend",(event)=>{
if(event.propertyName==="width"){
changeTransitionStatus(transitionStatus?false:true);
}
})
})
changeRenderStatus(true)
}
return (transitionStatus)?<div> First case </div>:<div> Second case</div>
}
there's another function with some DOM manipulation onMouseOver.
This function should change the state from the event listener but it doesn't.
you can't use hooks inside a if statement, see hooks-rules
you should return a clean up function from your useEffect hooks to remove the event listener and avoid memory leaks
you probably want the effect to run only once, so provide an empty array as second argument to useEffect (I don't think you need renderStatus)
inside the useEffect, when calling a state setter, prefer the functional form so that you always have a fresh state value.
example
const componentToRender = () => {
//const [renderStatus, changeRenderStatus] = useState(false);
const [transitionStatus, changeTransitionStatus] = useState(false);
// No condition
useEffect(() => {
const handler = (event) => {
if (event.propertyName === "width") {
//passe a function to state setter to get fresh state value
changeTransitionStatus(transitionStatus => transitionStatus ? false : true);
}
};
window.addEventListener("transitionend", handler);
// clean up
return () => window.removeEventListener("transitionend", handler);
}, []); // empty array => run only once
return (transitionStatus) ? <div> First case </div> : <div> Second case</div>
}

Categories