Search code examples
reactjsreduxredux-toolkitrtk-query

The updated value from the store does not change inside the function


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?)


Solution

  • 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 (
          ...
      );
    };