title: Stale Closure in React Context Provider
body: <p>I have a React Native application where I want to create a way to enable/disable notifications from multiple locations in the app flow. Enabling the notifications also involves holding on to some unsubscribe handlers that should be called during disabling to do the proper cleanup. It also needs to write/read into/from a react-redux store.</p>
  <p>For this I create a Context to share the functionality with arbitrary components in the app. You can see a simplified version below.</p>
  <p>I seem to run into a stale closure issue which I don't know how to resolve correctly. I have added comments which explain the issues inline in the code.</p>
  <p>If this is the wrong approach for this I would also be greatful for some insight in how to solve the stated use-case in a better way.</p>
  <pre class="lang-js prettyprint-override"><code>interface NotificationContextProps {
    enableNotifications: (promptForPermission: boolean) =&gt; Promise&lt;void&gt;;
    disableNotifications: () =&gt; void;
  }
  
  const SharedNotificationContext = React.createContext&lt;NotificationContextProps&gt;({
    enableNotifications: async () =&gt; {/**/},
    disableNotifications: () =&gt; {/**/},
  });
  
  export const SharedNotificationProvider: FunctionComponent = ({children}) =&gt; {
    const notificationListenerUnsub = useRef(() =&gt; {/**/});
    const tokenRefreshListenerUnsub = useRef(() =&gt; {/**/});
  
    // PROBLEM: Would have liked to use `useState` here instead of useRef
    // but the value change never reflected in the functions using the value.
    // Probably same stale closure issue as the other PROBLEM.
    const isEnabled = useRef(false);
  
    // PROBLEM: When udating the state with the `registerToken` function, this gets
    // updated correctly. When I place a `console.log(existingDeviceToken);` below this,
    // I can see that the change propagates here correctly.
    // BUT the `registerDeviceToken` function which does a check if the token changed,
    // (called by `enableNotifications` which is called on every app state change)
    // still uses the &quot;old&quot; `existingDeviceToken value` for the comparison
    const existingDeviceToken = useSelector((state: any) =&gt; state.firebase.token);
  
    const dispatch = useDispatch();
    const registerToken = (token: string) =&gt; dispatch({ type: FirebaseEvents.TOKEN, payload: token });
  
    const registerDeviceToken = async () =&gt; {
      const deviceToken = await getFirebaseToken();
  
      // PROBLEM: Even though the `existingDeviceToken` changed,
      // it's still using the old value.
      if (deviceToken &amp;&amp; deviceToken !== existingDeviceToken) {
        registerToken(deviceToken);
      }
    };
  
    const enableNotifications = async (promptForPermission: boolean) =&gt; {
      const permissionStatus = await getPushPermission(promptForPermission);
  
      if (permissionStatus.hasPermission &amp;&amp; !isEnabled.current) {
        // Has Push Permissions &amp; Listeners have not yet been setup
        cleanupNotificationHandlers();
        await registerDeviceToken();
        await updateNotificationListeners();
        isEnabled.current = true;
      } else if (permissionStatus.hasPermission &amp;&amp; isEnabled.current) {
        // Has Push Permissions but Listeners already exist
        await registerDeviceToken();
      } else if (permissionStatus.authStatus === messaging.AuthorizationStatus.DENIED) {
        // Push Permissions were declined or removed
        disableNotifications(true);
      }
    };
  
    const disableNotifications = async (permissionsDeclined: boolean) =&gt; {
      await removeFirebaseToken();
      registerToken(&quot;&quot;);
      cleanupNotificationHandlers();
      isEnabled.current = false;
    }
  
    const updateNotificationListeners = async () =&gt; {
      // Create listener and store the unsub function (simplified for demo)
      notificationListenerUnsub.current = () =&gt; {};
      tokenRefreshListenerUnsub.current = () =&gt; {};
    };
  
    const cleanupNotificationHandlers = () =&gt; {
      notificationListenerUnsub.current();
      notificationListenerUnsub.current = () =&gt; {/**/};
      tokenRefreshListenerUnsub.current();
      tokenRefreshListenerUnsub.current = () =&gt; {/**/};
      isEnabled.current = false;
    };
  
    return (
      &lt;SharedNotificationContext.Provider value={{ 
        enableNotifications, 
        disableNotifications 
      }}&gt;
        {children}
      &lt;/SharedNotificationContext.Provider&gt;
    );
  };
  
  export const useNotifications = () =&gt; {
    return React.useContext(SharedNotificationContext);
  };
  </code></pre>
  <p>I have tired wrapping the functions with <code>useCallback</code> like this:</p>
  <pre class="lang-js prettyprint-override"><code>const registerDeviceToken = useCallback(async () =&gt; {/**/}, [existingDeviceToken]);
  const enableNotifications = useCallback(async (promptForPermission: boolean) =&gt; {/**/}, [registerDeviceToken]);
  </code></pre>
  <p>But that did not help.</p>
  <p>What does work is packing the <code>existingDeviceToken</code> into a <code>useRef</code> and then using a</p>
  <pre class="lang-js prettyprint-override"><code>useEffect(() =&gt; {existingDeviceTokenRef.current = existingDeviceToken}, [existingDeviceToken]);
  </code></pre>
  <p>to update it. But that does feel like it's only a workaround and not a real solution.</p>
  <p>As you might have gathered, React is still new to me so I am also not 100% sure how to do this in a way to minimize changing the value prop too much so the provider won't cause the app to re-render.</p>