why updated state not reflected inside an event listener: React Native, Hooks
Asked Answered
S

2

8

I'm using hooks for updating state. in my code I have an AppState event listener and whenever it triggers I'm updating the appState using setAppState, however the appState inside the event listener did not change. but the value is updating outside the listener. can anyone explain why is behaving like that?

Here is my code:

    import React, { FunctionComponent, useEffect, useState } from "react"
    import { View, AppState, AppStateStatus } from "react-native"
    const Test: FunctionComponent<any> = (props: any) => {
        const [appState, setAppState] = useState < AppStateStatus > (AppState.currentState)
    
        useEffect(() => {
            AppState.addEventListener("change", _handleAppStateChange)
        },[])
    
        const _handleAppStateChange = (nextAppState: AppStateStatus) => {
         //appState not Changing here
            console.log(appState, "app state")
            if (appState.match(/inactive|background/) && nextAppState === "active") {
                console.log("App has come to the foreground!")
                activateRealtime()
            } else if (appState === "active" && nextAppState.match(/inactive|background/)) {
                console.log("App has come to background!")
            }
            setAppState(nextAppState)
        }
       //appState updated here
       console.log(appState, "app state")
        return <View />
    }
Succory answered 7/7, 2020 at 10:3 Comment(14)
You should fix this const [appState, setAppState] = useState < AppStateStatus > AppState.currentStateOdetteodeum
How do you know the state isn't updating inside the callback? setAppState(nextAppState) is the last call within the function.Gusty
its not the issue. changed it. i just forgot to include parantheses there.Succory
@DrewReese: in console the value did not change inside the listener.Succory
State updates are asynchronous. The state value doesn't change immediately any more than it would if you made an HTTP request that changed a variable.Confuse
That's what I refer to, the log is before you update state, so what bar are you using to assert state isn't updating? BTW, it still won't log updated state even if it was after the setAppState(nextAppState) since it'll only log state from the current render cycle.Gusty
@DrewReese. Oh i see. but whenever it updates. the value is still fix on every setState.Succory
The console log inside the event listener will always show the previous state, not the current state. The current state is stored in nextAppState.Blowhard
You can either log appState in an effect, i.e. useEffect(() => console.log(appState, "app state"), [appState]); since this will be the updated state or just log nextAppState in the callback since that is what you will update state to anyway.Gusty
@DrewReese. I already did that. hmm. i just used this code for example. im just saying that i cannot use appState inside the listener. because the value is always fix.Succory
@DrewReese. so you mean i need to put my condition of my listener inside useEffect(() => console.log(appState, "app state"), [appState]);? it's that the better way?Succory
Yes, that is the more correct way to log state changes. I'm concerned with what you mean by "always fix". Are you saying state (appState) never updates?Gusty
@DrewReese. It updates. but inside the listener the value is still same.Succory
@JaredSmith State updates are asynchronous. even though that is true, saying that will cause people to try await setState. Also the main problem with setting state created with useState in functional components is stale closuresComprise
C
7

In your code appState is a stale closure the linter should have told you that you have missing dependencies.

I think the following will work

const _handleAppStateChange = useCallback(
  (nextAppState) =>
    //use callback for state setter so you don't
    //  create needless dependency or (as you did)
    //  create a stale closure
    setAppState((currentAppState) => {
      //logs current appstate
      console.log(currentAppState, 'app state');
      if (
        currentAppState.match(/inactive|background/) &&
        nextAppState === 'active'
      ) {
        console.log('App has come to the foreground!');
        activateRealtime();
      } else if (
        currentAppState === 'active' &&
        nextAppState.match(/inactive|background/)
      ) {
        console.log('App has come to background!');
      }
      return nextAppState;
    }),
  //only pass function as _handleAppStateChange
  //  on mount by providing empty dependency
  []
);
useEffect(() => {
  AppState.addEventListener(
    'change',
    _handleAppStateChange
  );
  //clean up code when component unmounts
  return () =>
    AppState.removeEventListener(
      'change',
      _handleAppStateChange
    );
  //_handleAppStateChange is a dependency but useCallback
  //  has empty dependency so it is only created on mount
}, [_handleAppStateChange]);
Comprise answered 7/7, 2020 at 10:54 Comment(0)
E
0

Acceptable Solution

I was doing what the accepted answer suggested but it was still not working for me. I not only needed to call useCallback but I also needed to add my element's ref to the useEffect's dependency array as it was changing at some point and so the triggered callback was still using old data. I'm not sure how the callback was even being triggered then if element.current was replaced...

const elementRef = useRef(null);

useEffect(() => {
  elementRef.current.addEventListener("change", _handleAppStateChange)
  return () => elementRef.current.removeEventListener("change", _handleAppStateChange)
}, [elementRef.current])

...

return (
  <div ref={elementRef}>
  ...

Preferred Solution

However, as in my case, if you are rendering the element yourself, moving the event directly to the target element also fixes the issue. It not only makes the code simpler but it also just works whether you use useCallback or not!

const elementRef = useRef(null);

...

return (
  <div ref={elementRef} onChange={_handleAppStateChange}>
  ...

In this case, React will directly reattach the updated function to the element on every re-render.

Enchondroma answered 28/9, 2023 at 23:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.