React stale state useCallback - reactjs

I am using the geolocation API inside my functional component, and my success and error callbacks for geolocation.watchPosition(success, error) have conditional code that is dependent on some state value that is going to change.
I do not want to initiate geolocation.watchPosition on mount inside useEffect.
I tried wrapping the callbacks with useCallback and passing the state value to the dependency array, but the callback still closes over the stale state value even though it is updated. Is there any workaround to this?
Simplified example:
function Map() {
const watchRef = useRef();
const [isBool, setIsBool] = useState(false);
function init() {
watchRef.current = navigator.geolocation.watchPosition(success, error);
}
const success = useCallback((pos) => {
if(isBool) {
// do something...
return;
}
// do something else...
}, [isBool])
function updateStateHandler() {
setIsBool(true);
}
return // jsx...
}

Figured out a solution using refs to break out of the closure.
Solution:
function Map() {
const watchRef = useRef();
const isBoolRef = useRef(); // create a ref
const [isBool, setIsBool] = useState(false);
function init() {
watchRef.current = navigator.geolocation.watchPosition(success, error);
}
// no need for useCallback
const success = (pos) => {
if(isBoolRef.current) {
// do something...
return;
}
// do something else...
}
function updateStateHandler() {
setIsBool(true);
}
useEffect(() => {
isBoolRef.current = isBool; // this will return the updated value inside the callback
}, [isBool])
return // jsx...
}

Related

React can I create 'setState' function in custom hook?

I successfully applied local storage in my component and would like to create my first hook, which will take care of the local storage, so I can re-use it in my other components.
/* EXAMPLE */
//useState:
const [state,setState] = useState("");
setValue("Hello"); // WORKS!
//useLocalHook:
const [hook_value,setHookValue] = useLocalStorage("");
setHookValue("New Value"); // NOT WORKING
I read guides about custom hooks, but they are not using the setFunction for their custom hooks, is this impossible to do?
Parent:
// Local storage
const [data,setData] = useLocalStorage("mydata","");
useEffect(()=>{
setData(new_value);
},[data]);
Hook:
const useLocalStorage = (storage_key,initial_value) => {
const [value,setValue] = useState(getLocalStorage(storage_key,initial_value));
/* LOCAL STORAGE */
function saveLocalStorage(key,value){
if (typeof window !== "undefined") {
// Set New Default Value
const saved_value = JSON.stringify(value);
console.log(`Key:${key} stored:${value}`);
localStorage.setItem(key,saved_value);
}
}
function getLocalStorage(key,initial_value)
{
if (typeof window !== "undefined") {
const value = JSON.parse(localStorage.getItem(key));
console.log(`Key:${key} received:${value}`);
if(value)
{
return value;
}
}
// Not found, return initial value
return initial_value;
}
function clearLocalStorage()
{
localStorage.clear();
}
function setValue(new_val)
{
value = new_val;
}
// Save settings in local storage
useEffect(()=>{
saveLocalStorage(storage_key,value);
},[value]);
// Return value
return [value];
}
What am I not understanding /& doing wrong if it's possible to change the value with custom hooks?
You need to return setValuefn in your custom hook, so you can use used it inside useEffect
Hook:
const useLocalStorage = (storageKey,initialValue) => {
const [value,setValue] = useState(getLocalStorage(storageKey,initialValue));
return [value, setValue]
And now in the parent you can use setData
Parent
const [data,setData] = useLocalStorage("mydata","");
useEffect(()=>{
setData(new_value);
},[data]);

reactjs state is not being updated when invoked from a function

here is the context
I am invoking a function invoked(..) from another component. Inside invoked I setState but the state does not get set. It does not trigger the useEffect nor does it take the updated value in the useInterval.
Below is the code.
Component 1 = exampleComp.ts
const exampleComp = ({initValue: ExampleProps}) {
const [params, setParams] = useState<{field1: null | string}> ({field1: null});
const invoked = (x: string) => {
console.log("inside invoked"); // this gets printed
setParams({"field1": x});
};
useEffect(() => {
console.log(params); // does not come here :(
}, [params]);
const fetchData = {...};
useInterval(
() => {
if (!params.field1 || fetching) return;
console.log("in interval", params); // does not come here too :(
fetchData(params.field1);
},
params && !fetching ? 5000: null,
);
return {invoked, ...}
}
Component 2:
const newComp = ({initValue: ExampleProps}) {
const {invoked, ..} = useExampleComp({...}); //Example comp is the above component.
useEffect(() => {
invoked(x);
}, []);
}
Any help will be appreciated! thanks.
Turns out that the component was unmounting too early. This happens when the scope of the component is too narrow.
If you have a similar problem, try logging the component to see if this is indeed happening.

Best practice interface for custom hooks with function arguments

I'm creating a custom hook to detect clicks inside/outside a given HTMLElement.
Since the hook accepts a function as an argument, it seems like either the input needs to be wrapped in a useCallback or stored inside the hook with useRef to prevent useEffect from triggering repeatedly.
Are both of the following approaches functionally the same?
Approach One (preferred)
// CALLER
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside: () => {
// Do something via anonymous function
},
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
const onClickOutsideRef = useRef(onClickOutside);
onClickOutsideRef.current = onClickOutside;
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutsideRef.current && onClickOutsideRef.current();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement]);
}
Approach Two
// CALLER
const onClickOutside = useCallback(() => {
// Do something via memoized callback
}, []);
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside,
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutside();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement, onClickOutside]);
}
Does the first one (which I prefer, because it seems to make the hook easier to use/rely on fewer assumptions) work as I imagine? Or might useEffect suffer from enclosing stale function references inside handleClick?
useCallback is the right approach for this. However, I think I'd design it in a way where I could abstract the memo-ing away from the consumer:
/**
* #param {MutableRefObject<HTMLElement>} ref
* #param {Function} onClickOutside
*/
const useClickInsideOutside = (ref, onClickOutside) => {
const { current: htmlElement } = ref;
const onClick = useCallback((e) => {
if (htmlElement && !htmlElement.contains(e.target)) {
onClickOutside();
}
}, [htmlElement, onClickOutside])
useEffect(() => {
document.addEventListener(MOUSE_DOWN, onClick);
return () => document.removeEventListener(MOUSE_DOWN, onClick);;
}, [onClick]);
}
And since I already touched on design, I'd try to make the design resemble similar API functions where 2 arguments are [in]directly related. I'd end up looking like this:
// Consumer component
const ref = useRef()
useClickOutside(ref, () => {
// do stuff in here
})

Why using useRef in this react example? just for concept demostration?

Just wonder what purpose the useRef serve here in example: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
}
I tried and can achieve the same without useRef as below:
function Timer() {
const interval = null;
useEffect(() => {
const id = setInterval(() => {
// ...
});
interval = id;
return () => {
clearInterval(interval);
};
});
// ...
function handleCancelClick() {
clearInterval(interval);
}
// ...
}
So the saying "but it’s useful if we want to clear the interval from an event handler" from the react doc and this answer: Is useRef Hook a must to set and clear intervals in React?, just mean almost nothing at all.
It's fine only if you don't want stopping timer in handleCancelClick and keep all logic inside single useEffect(which would be really rare case).
See, if you get any re-render(because of any useState entry changed or props changed) between running timer and handleCancelClick you will get that variable const interval = null; and nothing will happen on click(clearTimeout(null); does nothing).
Don't see how that can be handled without preserving data between renders.

Any issues using useEffect this way?

Is there any potential issues handling componentDidMount and componentDidUpdate with useHooks in this manner?
Two goals here:
Use one useEffect to handle both componentDidMount and componentDidUpdate
No need to pass in the 2nd argument (normally an array with props)
const once = useRef(false)
useEffect(() => {
if(once.current === false){
once.current = true
// do things as were in componentDidMount
return
}
// do things as were in componentDidUpdate
// clean up
return () => {
//
}
}) // <- no need to pass in 2nd argument
There is no issue with it but if any props or state value will change then useEffect will not load on component re-render.
so better to use 2nd argument when this depends on the values change.
const once = useRef(false)
useEffect(() => {
if(once.current === false){
once.current = true
// do things as were in componentDidMount
return
}
// do things as were in componentDidUpdate
// clean up
return () => {
//
}
},[]) // some value as array who will change and this need to be called
Yopu should pass an empty array as an argument
useEffect(() => {
if(once.current === false){
// do things as were in componentDidMount
once.current = true
}
return () => {
// this area will fired when component unmounts
}
}, []) // by providing an empty array this useEffect will act like componentDidMount
If you want to re-render useEffect on a state change you can add the state to the empty array so it will listen changes in the state and will be executed
const [someStateValue, setSomeStateValue] = useState('')
useEffect(() => {
...
}, [someStateValue]) // will be executed if someStateValue changes
If you want to combine componentDidMount and componentDidUpdate, I have a solution in my mind that may work
const [val, setVal] = useState('asd')
const [oldVal, setOldVal] = useState('')
useEffect(() => { // componentDidMount
if(val !== oldVal){ // componentDidUpdate
// pass val to old val
setOldVal(val)
// set new val to new val
setVal("theNewestVal")
}
return () => { // componentWillUnmount
...
}
}, [val]) // will be executed if someStateValue changes

Resources