Stale custom hook state property on callback - reactjs

I have a custom hook in my React app that exposes a function (hookFn) to calculate a value. Once the value has been updated (state change, triggering useEffect), the hook alerts the app via a callback function. Here's the issue: in my callback function, I want to be able to access the value via hook.value, but it seems to be stale! Even though I know the value state has been updated!
Codesandbox: https://codesandbox.io/s/stoic-payne-bwp6j5?file=/src/App.js:0-910
import { useEffect, useRef, useState } from "react";
export default function App() {
const hook = useCustomHook();
useEffect(() => {
hook.hookFn(hookCallback);
}, []);
function hookCallback(value) {
console.log({givenValue: value, hookValue: hook.value});
}
return "See console for output";
}
function useCustomHook() {
const callbackRef = useRef(null);
const [value, setValue] = useState("initial value");
useEffect(() => {
if (callbackRef.current) {
callbackRef.current(value);
}
}, [value]);
function hookFn(callbackFn) {
callbackRef.current = callbackFn;
setValue("value set in hookFn");
}
return { hookFn, value };
}
FYI: in my actual app, the hook is for searching, which may call the callback function multiple times as more search results become available.
Is there any way to ensure hook.value will be valid? Or is it bad practice for a hook to expose a state variable in general?

It turns out hook.value is stale because hook is stale when I access it from hookCallback. Each time there is a state change within my custom hook, useCustomHook will generate a new object.
The complex solution, then, is to to create a ref for hook and keep it up to date in useEffect. But then I have to make sure I wait for that useEffect to run before accessing hookRef.current.value... Here's my attempt to make this work: https://codesandbox.io/s/dazzling-shirley-0r7k47?file=/src/App.js
However, a better solution: don't mix React states and manual callbacks. Instead, just watch for state changes in a useEffect, like so:
import { useEffect, useState } from "react";
export default function App() {
const hook = useCustomHook();
useEffect(() => {
hook.hookFn();
}, []);
useEffect(() => {
if (hook.value) console.log({ hookValue: hook.value });
}, [hook.value]);
return "See console for output";
}
function useCustomHook() {
const [value, setValue] = useState("initial value");
function hookFn(callbackFn) {
setValue("value set in hookFn");
}
return { hookFn, value };
}
Notice the code is simplified, and there's no need for concern about states being out-of-sync.

I think you have pretty much answered you own question. Alternatively, you could pass your callback function as input to your custom hook.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const hook = useCustomHook(hookCallback);
useEffect(() => {
hook.setNewValue();
},[])
function hookCallback(value) {
console.log({
givenValue: value,
hookValue: hook.value, // Why is this stale??
areIdentical: value === hook.value // Should be true!!!
});
}
return <h1>See console for output</h1>;
}
function useCustomHook(callback) {
const [value, setValue] = useState("initial value");
useEffect(() => {
callback(value);
}, [value]);
function setNewValue(callbackFn) {
setValue("value set in hookFn");
setTimeout(() => {
setValue("value set in setTimeout");
}, 100);
}
return { setNewValue, value };
}

Related

Cleanup custom event from ref in react

I have a custom element, that dispatch a custom event in react.
Since there is no way to bind the event simply, I need to create a eventListener manually and remove it.
I tried :
export function App() {
const textRef = useRef<DemoTextElement>(null);
let eventRef;
const logEvent = (e) => {
console.log(e)
}
useEffect(() => {
if(textRef.current) {
eventRef = textRef.current;
textRef.current.addEventListener('onCustomEvent', logEvent)
}
return () => {
if(eventRef) {
eventRef.removeEventListener('onCustomEvent', logEvent)
}
}
}, [])
return (
<div className={styles.app}>
<demo-text ref={textRef}/>
</div>
);
}
but I get prompted that
Assignments to the 'eventRef' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect.
but I am, already using useRef hook, so I don't exactly know how to fix this... Cause I can't add useRef hook inside useEffect hook.
The eventRef variable will be re-declared for each rendering. Just move it inside useEffect hooks and do the assignment operation.
import React, { useEffect, useRef } from 'react';
export default function Test() {
const textRef = useRef<HTMLDivElement>(null);
const logEvent = (e) => {
console.log(e);
};
useEffect(() => {
const eventRef = textRef.current;
if (eventRef) {
eventRef.addEventListener('onCustomEvent', logEvent);
}
return () => {
if (eventRef) {
eventRef.removeEventListener('onCustomEvent', logEvent);
}
};
}, []);
return <div ref={textRef}>123</div>;
}

Wrapping useState hook with custom setter and clean-up functionality

I'd like to create a custom React hook that
wraps a state (created with useState),
runs a clean-up function up when the component unmounts,
executes a custom setter logic that derives a value,
and exports the custom setter and the internal state value.
export function useCustomState() {
const [value, setValue] = useState();
useEffect(() => {
return () => {
// do cleanup
};
}, [value]);
function customSetValue(newValue) {
if (value) {
// do cleanup for the previous value
}
const derivedValue = derive(newValue);
setValue(derivedValue);
}
return [value, customSetValue];
}
I can achieve this with the code above. My problem arises when I use the returned values in an useEffect hook as a dependency, since the returned custom setter is always a new function reference.
const Component = () => {
const [value, setValue] = useCustomState();
useEffect(
() => {
setValue(simpleValue);
},
[setValue],
);
return <p>{ value }</p>;
};
When I don't include the setter as a dependency of the useEffect, the re-rendering stops, since the dependency does not change after a render. I can omit that reference and disable eslint for the line. This is one solution.
I'd like to know whether it is possible to create a custom referentially stable setter function?
I also have tried using useMemo that stores the custom setter function, but the dependencies for the memo still include the internal value and setValue references, since I want to do clean-up and set the new derived value. If the derived value is not the same for the same input, the dependency cycle will result in infinite re-rendering.
You can pass a function to setValue to remove the dependence on value; instead, the current value is passed as an argument. As a simpler alternative to useMemo, useCallback will give you a consistent function:
export function useCustomState() {
const [value, setValue] = useState();
useEffect(() => {
return () => {
// do cleanup
};
}, [value]);
const customSetValue = useCallback((newValue) => {
setValue((oldValue) => {
if (oldValue) {
// do cleanup for the previous value
}
const derivedValue = derive(newValue);
return derivedValue;
});
}, []);
return [value, customSetValue];
}

Export function from inside a React function

I have a function that handles React Native location. For demonstration:
const useLocation = () => {
const [fetchingLocation, setFetchingLocation] = useState(true);
...
const changeSystemPermissions = useCallback(() => {...});
useEffect(() => {
//does many things
}, [...])
}
I need to have the function changeSystemPermissions inside useLocation as it uses the state.
I realize that I can export the changeSystemPermissions function as a const with a return [changeSystemPermissions, ...] and then import it in another component with:
const [
changeSystemPermissions,
...
] = useLocation();
However, it will ALSO run the useEffect function. I do want it to run once, but I need to access changeSystemPermissions in several other components and I don't want the useEffect to run multiple times.
I was thinking I will just take out the changeSystemPermissions function outside of useLocation, but it needs to use the state. I suppose I COULD pass the state vars into the changeSystemPermissions when it is outside useLocation, but that would be verbose and ugly.
How can I export changeSystemPermissions and just that function without having to import the whole useLocation function?
Can you move the useEffect to the component one ?
const useLocation = () => {
const [fetchingLocation, setFetchingLocation] = useState(true);
const changeSystemPermissions = useCallback(() => {
...
});
const funcToExecute = useCallback(() => {
....
}, []);
return { changeSystemPermissions, funcToExecute }
}
And put it in the component :
const {
changeSystemPermissions,
funcToExecute,
} = useLocation();
useEffect(() => {
funcToExecute()
}, [...])
Also, if you really need the useEffect to be in the custom hook,
maybe you can add a param to this hook.
const useLocation = (shouldTriggerEffect) => {
const [fetchingLocation, setFetchingLocation] = useState(true);
const changeSystemPermissions = useCallback(() => {
...
});
useEffect(() => {
if (shouldTriggerEffect) {
...
}
}, [shouldTriggerEffect])
return { changeSystemPermissions, funcToExecute }
}
And then in the component,
const {
changeSystemPermissions,
} = useLocation(false);
Tell me if I misunderstood something or if it helps :)
When ever you call a hook inside a React functional component, it will create a new state for that hook and not sharing among components. But there is a library which could help you achieve that:
https://github.com/betula/use-between
You could follow example to use this library or maybe just read the code and utilize the approach for your case to share the hook state between components.

I am trying to use callback in hook but can not get latest context value in the callback

const Demo = () => {
const { name } = useContext(AppContext);
function emiterCallback(val) {
console.log('value==', name);
if (name !== val) {
setContextState({ name: val });
}
}
useEffect(() => {
window.eventEmitter.on('CHANGED', emiterCallback);
return () => {
window.eventEmitter.removeListener('CHANGED', emiterCallback);
};
}, []);
}
in class component
this.emiterCallback = this.emiterCallback.bind(this) can solve my question, but how to use it in hook ?
The problem you have here is due to the fact that useEffect with an empty array dependency only runs once - when the component mounts. This means that the emiterCallback it assigns as the event function is the very first one that's made on the first render. Since you just declare emiterCallback in the body of the function, it gets remade every single re-render, so after a single re-render, it will be a different one to the event one you assigned when the component mounted. Try something like this:
import React, { useCallback, useContext, useEffect } from 'react';
...
const Demo = () => {
const { name } = useContext(AppContext);
// Assign it to a memoized function that will recalculate as needed when the context value changes
const emiterCallback = useCallback((val) => {
console.log('value==', name);
if (name !== val) {
setContextState({ name: val });
}
}, [name]);
// Adding the function as a dependency means the .on function should be updated as needed
useEffect(() => {
window.eventEmitter.on('CHANGED', emiterCallback);
return () => {
window.eventEmitter.removeListener('CHANGED', emiterCallback);
};
}, [emiterCallback]);
}
This code isn't tested but you get the idea
use useCallback to memorize the effect no need for bind since there is no this as it is not a class,
Here read more about it -
How can I bind function with hooks in React?

How can I check if the component is unmounted in a functional component?

A callback function sets the component state. But sometimes subscription which supplies the data need to end. Because callback is executed asynchronously, it's not aware if the subscription ends just after making the service call (which executes the callback function).
Then I see following error in the console:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
Is there a way to access the component state, even if I am in the callback function?
This would be the steps:
subscribe with parameters
unsubscribe
component is unmounted
subscribed service executes the callback function
callback functio sets state in an unmounted component and it gives error above
You can use a ref like this:
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => { mounted.current = false; };
}, []);
Then in your callback you can check if mounted.current === false and avoid setting the state
Here is some pseudo code how you can use useEffect to see if a component is mounted.
It uses useEffect to listen to someService when it receives a message it checks if the component is mounted (cleanup function is also called when component unmounts) and if it is it uses setServiceMessage that was created by useState to set messages received by the service:
import { useState, useEffect } from 'react';
import someService from 'some-service';
export default props => {
const userId = props.userId;
const [serviceMessage, setServiceMessage] = useState([]);
useEffect(
() => {
const mounted = { current: true };
someService.listen(
//listen to messages for this user
userId,
//callback when message is received
message => {
//only set message when component is mounted
if (mounted.current) {
setServiceMessage(serviceMessage.concat(message));
}
});
//returning cleanup function
return () => {
//do not listen to the service anymore
someService.stopListen(userId);
//set mounted to false if userId changed then mounted
// will immediately be set to true again and someService
// will listen to another user's messages but if the
// component is unmounted then mounted.current will
// continue to be false
mounted.current = false;
};
},//<-- the function passed to useEffects
//the function passed to useEffect will be called
//every time props.userId changes, you can pass multiple
//values here like [userId,otherValue] and then the function
//will be called whenever one of the values changes
//note that when this is an object then {hi:1} is not {hi:1}
//referential equality is checked so create this with memoization
//if this is an object created by mapStateToProps or a function
[userId]
);
};
This hook (insired from Mohamed answer) solves the problem in a more elegant maner:
function useMounted() {
const mounted = useMemo(() => ({ current: true }), []);
useEffect(() => {
return () => { mounted.current = false}
}, [mounted]);
return mounted;
}
(Updated to use useMemo instead of useRef for readability).
You can return a function from useEffect, which will be fired when a functional component unmount.
Please read this
import React, { useEffect } from 'react';
const ComponentExample = () => {
useEffect(() => {
// Anything in here is fired on component mount.
return () => {
// Anything in here is fired on component unmount.
}
}, [])
}
I found the accepted answer to this question hard to read, and React provides their own documentation on just this question. Their example is:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
I've created a component I call <Fade> that will fade in/out any children its given. Note that it relies on bootstrap's .fade and .show classes, though these could easily be implemented without bootstrap.
const Fade: React.FC<{}> = ({ children }) => {
const [ className, setClassName ] = useState('fade')
const [ newChildren, setNewChildren ] = useState(children)
useEffect(() => {
setClassName('fade')
const timerId = setTimeout(() => {
setClassName('fade show')
setNewChildren(children)
}, TIMEOUT_DURATION)
return () => {
clearTimeout(timerId)
}
}, [children])
return <Container fluid className={className + ' p-0'}>{newChildren}</Container>
}
It all boils down to one rule: unsubscribe from your asynchronous tasks in the cleanup function returned from useEffect.

Resources