Cannot for the life of me figure out what is going on, but for some reason when "Click me" is clicked, the number increments as you'd expect. When a click is triggered by the Child component, it resets the state and ALWAYS prints 0.
function Child(props: {
onClick?: (id: string) => void,
}) {
const ref = useCallback((ref) => {
ref.innerHTML = 'This Doesnt';
ref.addEventListener('click',() => {
props.onClick!('')
})
}, [])
return (<div ref={ref}></div>)
}
function Parent() {
const [number, setNumber] = useState(0);
return <div>
<div onClick={() => {
setNumber(number + 1);
console.log(number);
}}>
This Works
</div>
<Child
onClick={(id) => {
setNumber(number + 1);
console.log(number);;
}}
/>
</div>
}
And here is a demonstration of the problem: https://jscomplete.com/playground/s333177
Both the onClick handlers in parent component are re-created on every render, rightly so as they have a closure on number state field.
The problem is, the onClick property sent to Child component is used in ref callback, which is reacted only during initial render due to empty dependency list. So onClick prop received by Child in subsequent renders does not get used at all.
Attempt to resolve this error, by either removing dependency param or sending props.onClick as in dependency list, we land into issue due to caveat mentioned in documentation. https://reactjs.org/docs/refs-and-the-dom.html
So you add null handling, and you see your updated callback now getting invoked, but... all earlier callbacks are also invoked as we have not removed those event listeners.
const ref = useCallback((ref) => {
if(!ref) return;
ref.innerHTML = 'This Doesnt';
ref.addEventListener('click',() => {
props.onClick!('')
})
}, [props.onClick])
I believe this just an experiment being done as part of learning hooks, otherwise there is no need to go in roundabout way to invoke the onClick from ref callback. Just pass it on as prop to div.
Edit:
As per your comment, as this is not just an experiment but simplification of some genuine requirement where click handler needs to be set through addEventListener, here is a possible solution:
const ref = useRef(null);
useEffect(() => {
if(!ref.current) return;
ref.current.innerHTML = 'This Doesnt';
const onClick = () => props.onClick!('');
ref.current.addEventListener('click',onClick)
// return the cleanup function to remove the click handler, which will be called before this effect is run next time.
return () => {ref.current.removeEventListener("click", onClick)}
}, [ref.current, props.onClick]);
Basically, we need to use useEffect so that we get a chance to remove old listener before adding new one.
Hope this helps.
function Child(props: {
onClick?: (id: string) => void,
}) {
function handleClick() {
props.onClick('')
}
const ref = useRef();
useEffect(() => {
ref.current.innerHTML = 'This Doesnt';
ref.current.addEventListener('click', handleClick)
return () => { ref.current.removeEventListener('click', handleClick); }
}, [props.onClick])
return (<div ref={ref}></div>)
}
#ckder almost works but console.log displayed all numbers from 0 to current number value.
Issue was with event listener which has not been remove after Child component umount so to achive this I used useEffect and return function where I unmount listener.
Related
Across my app, there are some UX logic that needs to be shared. They are triggered by events, so I wrote 2 custom hooks. Let's call one useRefWithCalc. The other hook is a more standard useEventListener, similar to this one.
useRefWithCalc calls the native useRef, has some internal handlers for UX, then calls useEventListener to attach those handlers. useRefWithCalc returns the ref created within, so another component can use this hook, and get the returned ref to attach to elements.
This has worked for me when the ref isn't attached to conditionally rendered elements.
The component looks something like this. Please take note on the 2 test logs.
const useEventListener = (event, listener, ref) => {\
...
useEffect(() => {
...
console.log("1. ref is: ", ref.current); // test logging 1.
ref.current.addEventListener(event, listener);
return () => {
ref.current.removeEventListener(event, listener);
}
}, [event, listener, ref]);
}
const useRefWithCalc = (value) => {
const ref = useRef(null);
...
const calc = () => {
// some calculations
}
...
useEventListener(event, calc, ref)
return [ref, result]
}
// works perfectly
const WorkingElement = (props) => {
const [ref, result] = useRefWithCalc(props.value);
...
return <B ref={ref} />
}
// doesn't work consistently
const ConditionalElement = (props) => {
const [state, setState] = useState(false);
const [ref, result] = useRefWithCalc(props.value)
useEffect(()=>{
if (ref && ref.current) {
ref.current.focus();
console.log("2. ref is: ", ref.current); // test logging 2
}
}, [ref])
...
return state ? <A> : <B ref={ref} />
}
The <WorkingElement /> works just as expected. The ref gets attached, and handles events with no problem.
However, in the <ConditionalElement />, when B is mounted, sometimes times test logging 1 won't fire. Test logging 2 always fires, and the ref gets the focus correctly. But this update is not passed into useEventListener
Once <B /> gets 1 subsequent update (e.g. when user inputs something), both logs will fire correctly, and the event listner gets attached correctly, and it work just as <WorkingElement />
Sorry for not posting the exact code. I feel like my approach is convoluted and might be wrong.
In React when a ref changes, it doesn't trigger a component update, and useEffects are not triggered.
I suggest to put your ref inside a state so that effects are triggered when the ref changes :
const [ref, setRef] = useState(undefined)
return (
<B ref={setRef}/>
)
Everytime I work with addEventListener(), and also want to access some state inside useEffect, I get the same issue. I can't add the state as dependency, because then I would create multiple event listeners each time the state changes.
I almost everytime find myself stuck with the "React Hook useEffect has a missing dependency" warning.
Let's say I have a component that needs to change it state on window.onClick() and on window.onDoubleClick(). If the state is true, click should change it to false, and if the state is false, double click should change it to true.
So here's what I whould write:
import React, { useState, useEffect } from 'react';
export default function someComponent() {
const [toggle, setToggle] = useState(false);
useEffect(() => {
window.addEventListener('click', (event) => {
if (toggle) setToggle(false)
})
window.addEventListener('dblclick', (event) => {
if (!toggle) setToggle(true)
})
}, [])
return (
<p>The toggle state is {toggle.toString()}</p>
);
}
This code works, but I get the missing dependency warning. I can't add toggle to the dependency array, because then it will add another event listener each time the toggle state changes.
What am I doing wrong here? how should I fix this?
Edit: Maybe this example wasn't too good, but it's the simplest I could think of. But, this issue is also for when I create other event listeners, that have to be on the windows object, like scroll. I know I can use return to remove the event listener everytime, but for events like scroll it makes it much slower. It doesn't make sense to me that I have to remove and add it everytime, when I just don't need it to fire again.
With react you don't have to use the window element in this case. Not even a useEffect.
By using the useEffect hook you are telling react to do something after render (depending on the dependency array). In this case changing state is not necessary immediately after rendering the page, only when the user interacts with the element.
Adding click events through the useEffect is probably not needed most of the time and and doing it like the example below will probably save you time and a headache and maybe even performance (correct me if i'm wrong).
I would personally do it like this.
import React, { useState } from 'react';
export default function someComponent() {
const [toggle, setToggle] = useState(false);
return (
<p
onClick={() => setToggle(false)}
onDoubleClick={() => setToggle(true)}
>
The toggle state is {toggle.toString()}
</p>
);
}
You could also call functions from the element like so
const [toggle, setToggle] = useState(false);
const handleClick = () => {
if (toggle) {
setToggle(false);
}
};
const handleDoubleClick = () => {
if (!toggle) {
setToggle(true);
}
};
return (
<p
onClick={() => handleClick()}
onDoubleClick={() => handleDoubleClick()}
>
The toggle state is {toggle.toString()}
</p>
);
CodeSandbox example
You can add a clean-up function to the useEffect hook to remove old listeners. This way you can pass toggle into the dependency array and you won't have stacking event listeners.
https://reactjs.org/docs/hooks-effect.html
useEffect(() => {
const handleClick = () => toggle ? setToggle(false) : setToggle(true);
window.addEventListener('click', handleClick);
window.addEventListener('dblclick', handleClick);
return () => {
window.removeEventListener('click', handleClick);
window.removeEventListener('dblclick', handleClick);
}
}, [toggle]);
I can't add the state as dependency, because then I would create multiple event listeners each time the state changes.
There is a way around this, and that is to return a cleanup function from the useEffect callback. I would encourage you to read the linked section of the docs, then the below solution would become much clearer:
useEffect(() => {
const handleClick = () => {
setToggle(!toggle)
}
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [toggle])
with the above solution, each time toggle is updated, the cleanup function is called, which removes the current event listener before running the effect again.
Also note that you can provide a callback function to setToggle, which receives the current value of toggle and returns the new value. With this approach you wouldn't need to pass toggle as a dependency to useEffect:
useEffect(() => {
const handleClick = () => {
setToggle(currentValue => !currentValue)
}
window.addEventListener("click", handleClick)
return () => {
window.removeEventListener("click", handleClick)
}
}, [])
I want to create a custom hook in which I add a click event listener to a DOM element which calls a function defined in a React component which uses a state variable.
I add the event listener, but when I call the function, it does not reflect any state changes, it is always taking the initial state value.
const useCustomHook = (functionDefinedInComponent) => {
// logic to get the dom element
element.addEventListener('click', () => functionDefinedInComponent(item));
};
const CustomComponent = () => {
const [state, setState] = useState(...);
const customFunction = (item) => {
setState(...); // change the 'state' variable
// use item and state to do something
}
useCustomHook(customFunction);
return ...;
}
When I click the DOM element to which I added the click event, the customFunction triggers with initial state value. Is there any to solve this?
I meant something like this.
you might have to wrap your callback function in React.useCallback as well.
const useCustomHook = (functionDefinedInComponent) => {
React.useEffect(() => {
// logic to get the dom element
element.addEventListener('click', () => functionDefinedInComponent());
}, [functionDefinedInComponent])
};
Can you try this out and let me know what sort of problem you get.
Here is a code sandbox that you were trying to do.
https://codesandbox.io/s/rakeshshrestha-nvgl1?file=/src/App.js
Explanation for the codesandbox example
Create a custom hook
const useCustomHook = (callback) => {
React.useEffect(() => {
// logic to get the dom element
const el = document.querySelector(".clickable");
el.addEventListener("click", callback);
// we should remove the attached event listener on component unmount so that we dont have any memory leaks.
return () => {
el.removeEventListener("click", callback);
};
}, [callback]);
};
so, I created a custom hook named useCustomHook which accepts a function as a parameter named callback. Since, we want to attach an event on element with class clickable, we should wait till the element gets painted on the browser. For this purpose, we used useEffect which gets called after the component has been painted on the screen making .clickable available to select.
const [input, setInput] = React.useState("");
const logger = React.useCallback(() => {
alert(input);
}, [input]);
useCustomHook(logger);
// render
Here, I have a state input which holds the state for the textbox. And also a function named logger which I passed to my custom hook. Notice, that the function logger has been wrapped inside of useCallback. You don't need to wrap it in this case, but it was there so that every time the component rerenders a new logger function won't be created except the changes in the input state.
You can use a public component like this:
const ClickableComponent = props => {
const { title, handleClick, component: Component } = props;
return (
<Component onClick={handleClick}>{title}</button>
)
};
export default ClickableComponent;
You can use this component like below:
<ClickableComponent title="your title" handleClick={handleClick} component={<button/> } />
I encountered this behavior. Two onChange are called in turn. One of them saves data to the state, and use the previus state to generate new one.
const onChange = (data) => {
setState1((prev) => {
console.log("onChange set state");
return [...prev, data];
});
};
The other just prints something to the console.
const onChange2 = (data) => {
console.log("onChange2");
};
onChange and onChange2 called after click on button
<button
onClick={() => {
onChange({ foo: "bar" });
onChange2();
}}
>
2
</button>
useEffect - saves data to state
const [state, setState] = useState(0);
const [state1, setState1] = useState([]);
useEffect(() => {
setState((prev) => {
console.log("use effect set state");
return prev + 1;
});
}, []);
So, if you do not use useEffect (comment out), the console log, which in the first onChange will be called first, then the console log from the second onChange
If you uncomment useEffect, then the console will log from onChange2 first, then from onChange
Why is this happening, who can tell?
CodeSandBox: https://codesandbox.io/s/eager-hooks-sczvb
The call order of onChange and onChange2 appear to change because the console.log in onChange is inside the callback passed to setState1.
If you move the console.log directly in the onChange you will see that the order is the same with or without useEffect.
const onChange = (data) => {
console.log("onChange set state");
setState1((prev) => {
return [...prev, data];
});
};
It's important to understand that when you call setState there is no guarantee that the state will be changed immediately, it's up to React to decide when to apply the new state and trigger a new render.
This is why the callback passed to setState should only return a new state and not run any effects (such as console.log) since the moment this code is run is considered in implementation detail and could change with any version of React.
Expected behavior is onChange2 called first, and onChange set state called second whatever useEffect body is commented. because onChange2 called right now. but onChange set state in setState fn called after rendered。
The "bug" looked is why onChange set state called first when first click. Hmm... find react computeExpirationForFiber. setState callback execute sync or async by different conditions. It's hard to read.
I have a state value ccFormDetails with a an empty object as default value.
const [ccFormDetails, setCCFormDetails] = useState({})
In first useEffect, I call a function the populates ccFormDetails with relevant data.
const getCCFormData = async () => {
const ccFormResult = await ContractService.getContractsCCFormData()
const { ccFormData } = ccFormResult
setCCFormDetails(ccFormData)
}
In second useEffect, I create an a event named message and assign a handleCgEvent handler to it.
useEffect(() => {
window.addEventListener('message', handleCgEvent)
return () => window.removeEventListener('message', handleCgEvent)
}, [])
I'm rendering an iframe with a submit button that emits the message event when clicked.
Then, when I click the sumbit button in the iframe, the handleCgEvent handler fires and should extract the correct (updated, fecthed) value of ccFormDetails. (Which I can see populated correctly in the React components tree)
const handleCgEvent = e => {
if (e.data === 'reload_cg') {
console.log('fail')
}
if (e.data['event_id'] === 'cg-success') {
console.log('success')
console.log('ccFormDetails1 ', ccFormDetails)
}
}
But what I get is {} meaning the original default state.
This should not happen per my knowledge of React.
Am I missing something/Does event handling messes state?
The problem here is you're using the handleCgEvent that's created when the component is mounted, and ccFormDetails is encapsulated within it using its default value. In order to get the most current state, you're going to have to use useRef. Something like:
const [ccFormDetails, setCCFormDetails] = useState({})
const formRef = useRef();
formRef.current = ccFormDetails;
const handleCgEvent = e => {
if (e.data === 'reload_cg') {
console.log('fail')
}
if (e.data['event_id'] === 'cg-success') {
console.log('success')
console.log('ccFormDetails1 ', formRef.current)
}
}
useEffect(() => {
window.addEventListener('message', handleCgEvent)
return () => window.removeEventListener('message', handleCgEvent)
}, []);
If it's okay for you to add and remove the event listener, then I would just add [ccFormDetails] as a dependency, assuming the entire object is recreated when its properties change.
For further reading about stale values in closures, this is a great blog entry: https://dmitripavlutin.com/react-hooks-stale-closures/