Light-Keeper / react-singleton-hook

Create singleton hook from regular react hook
MIT License
238 stars 13 forks source link

Redux state is outdated in functions created inside a singleton-hook #498

Closed harrynikolov closed 1 year ago

harrynikolov commented 2 years ago

Hello! We have a strange bug, where functions created inside a singleton-hook appear to be outdated. We have proved that the singleton-hook recognizes a Redux state update AND re-renders. But then, when one of the singleton-hook's functions is called by a consumer, it does not appear to be calling the most up-to-date version of the function.

I'm wondering if you can help? Do singleton-hooks memoize their outputs behind the scenes; or what else could be causing the singleton-hook to return a stale function?

We have confirmed that making this hook to NOT be a singleton-hook solves the problem. (see console.logs below)

Attached below is some example code (contains some pseudocode for simplicity)

const initialReduxState = {
  currentRound: undefined,
};

// ---

const useDriversRoundsPermissionsImpl = () => {
  const currentRound = useSelector(selectCurrentRound);

  const hasPermission = listenToIndexedDbChanges('hasPermission', currentRound?.id);

  useEffect(() => {
    if (currentRound) console.log(currentRound);
    // OUTPUT: { roundId: 309823, roundName: "Round 1" }
    // Prints 1st, suggesting a re-render isrequestPermissionForRound() should contain the updated state.
  }, [currentRound]);

  const requestPermissionForRound = async () => {
    const test = selectCurrentRound(store.getState());
    console.log(currentRound === undefined, test === undefined);
    // OUTPUT: true, false
    // When fetched with store.getState() the currentRound is NOT undefined.
    // OUTPUT WHEN NOT A SINGLETON-HOOK: false, false
  };

  return { hasPermission, requestPermissionForRound };
};

export const useDriversRoundsPermissions = singletonHook(
  {},
  useDriversRoundsPermissionsImpl
);

// ---

function RoundRequestManager() {
  const dispatch = useDispatch();  
  const currentRound = useSelector(selectCurrentRound);

  const { hasPermission, requestPermissionForRound } = useDriversRoundsPermissions();

  useEffect(() => {
    if (currentRound && !hasPermission) {
      requestPermissionForRound();
      // We can say with certainty that currentRound is NOT undefined when this is called
    }
  }, [hasPermission, currentRound]);

  return (
    <>
      <button
        onClick={() =>
          dispatch(setCurrentRound({ roundId: 309823, roundName: "Round 1" }))
        }
      >
        Join Round 1
      </button>

      <button
        onClick={() =>
          dispatch(setCurrentRound({ roundId: 981726, roundName: "Round 2" }))
        }
      >
        Join Round 2
      </button>
    </>
  );
}
Light-Keeper commented 1 year ago

Hi @harrynikolov, the problem might reside in your code, RoundRequestManager component:

  useEffect(() => {
    if (currentRound && !hasPermission) {
      requestPermissionForRound();
      // We can say with certainty that currentRound is NOT undefined when this is called
    }
  }, [hasPermission, currentRound]);

It uses requestPermissionForRound but not declares it as a dependency. This code still works if currentRound changes at the same render cycle together with requestPermissionForRound, but it is not the case for react-singleton-hook. The library introduces a layer of indirection to allow many subscribers and will trigger another render with a new value when it propagates.

I hope it solves the problem. If it is not, and you still believe there is a caching issue incidence the library, I'd be happy to debug an MRE.

mkhbragg commented 1 year ago

@Light-Keeper I have encountered this issue as well. Please see https://stackblitz.com/edit/react-ts-merrh3?file=hooks/useTest.ts.

This is representative of my use-case, and as you can see the value that is included as a dependency of the useCallback is not updated, even though the useEffect in that same file gets the updated value.

And just in case the issue was with my implementation of ReduxStore, here is another example in which I use react-redux library directly, and wrap <SingletonHooksContainer/> in the react-redux Provider and the behavior is the same.

Please advise 🙏

Yikes, nevermind. The listener created a closure that no longer had access to the state being updated. I feel like I just failed a coding interview 🤦‍♀️ Please disregard.