I have some functional component. Inside component I get value from redux store (I am using redux-toolkit). Also I have handler inside this component.
The value of variable from store set after request to api via RTK Query. So, the variable first has a default value, and then changes to value from the api.
Problem: The value of variable from redux store doesn't updated inside handler.
const SomeContainer = () => {
const dispatch = useDispatch();
const variableFromStore = useSelector(someSelectors.variableFromStore);
console.log(variableFromStore) **// correct value (updated)**
const handleSomeAction = () => {
console.log(variableFromStore) **// default value of init store (not updated)**
};
return <SomeComponent onSomeAction={handleSomeAction} />;
};
SomeComponent
const SomeComponent = (props) => {
const { list, onSomeAction } = props;
const moreRef = useRef(null);
const loadMore = () => {
if (moreRef.current) {
const scrollMorePosition = moreRef.current.getBoundingClientRect().bottom;
if (scrollMorePosition <= window.innerHeight) {
onSomeAction(); // Call handler from Container
}
}
};
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
};
}, []);
return (
...
);
};
How is it possible? What do I not understand?)
The problem is you're unintentionally creating a closure around the original version of handleSomeAction
:
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
}
}, []);
The dependencies array here is empty, which means this effect only runs the first time that your component mounts, hence capturing the value of loadMore
at the time the component mounts (which itself captures the value of onSomeAction
at the time the component mounts).
The "easy fix" is to specify loadMore
as a dependency for your effect:
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
}
}, [loadMore]);
BUT! This will now create a new problem - handleSomeAction
is recreated on every render, so your effect will now also run on every render!
So, without knowing more details about what you're actually trying to do, I'd use a ref
to store a reference to the onSomeAction
, and the inline the loadMore
into your effect:
// A simple custom hook that updates a ref to whatever the latest value was passed
const useLatest = (value) => {
const ref = useRef();
ref.current = value;
return ref;
}
const SomeComponent = (props) => {
const { list, onSomeAction } = props;
const moreRef = useRef(null);
const onSomeActionRef = useLatest(onSomeAction);
useEffect(() => {
const loadMore = () => {
if (!moreRef.current) return;
const scrollMorePosition = moreRef.current.getBoundingClientRect().bottom;
if (scrollMorePosition <= window.innerHeight) {
onSomeActionRef.current();
}
}
window.addEventListener('scroll', loadMore);
return () => window.removeEventListener('scroll', loadMore);
}, []);
return (
...
);
};