简体   繁体   中英

Why can't I update the state of my component from within a useEffect callback?

I am trying to create a timer that is started once the app is in the background, and then when the app is called to the foreground again the app checks if it has been 15 minutes (for ease of testing I am using 15s currently). If it has the app logs the user out, else the timer resets.

If this is a dupe please refer me - I just couldn't find anything. I've been trying to work this out for a couple of days now.

Here is my thinking:

  • I listen in to the app state change using AppState.addEventListener in useEffect. This fires a callback function on change which allows me to access the actual state that it has changed to.
  • I store Date.now() of when the user puts the app into the background in a setState hook.
  • When the app is called into the foreground, I then do Date.now() minus the stored state to work out if the time has elapsed.

Here is my problem:

  • When I attempt to reset the timer using setState from within the useEffect callback, it does nothing. I can't think of how else I'm supposed to be updating the state to do this. I have tried to also make these calls as references to other functions and it does the same thing

Things to note:

  • This is being handled on my root app navigator one the user is authenticated. Pretty sure this is irrelevant, but just in case anyone was wondering.
  • I have tried doing this using redux and it's the same issue. Dispatching state updates do not get executed.
  • I am using an expo managed workflow, so if you have any alternative npm suggestions they need to be compatible.

const rootNavigator = () => {

  const dispatch = useDispatch()
  const [loginTimer, setLoginTimer] = useState(Date.now())

  useEffect(() => {
    AppState.addEventListener('change', handleAppStateChange)

    return (() => {
      AppState.removeEventListener('change', handleAppStateChange)
    })
  }, [])

  const handleAppStateChange = (state) => {

    if (state === "inactive" || state === "background") {
        setLoginTimer(Date.now())
      }
    if (state === "active") {
      if (Date.now() - loginTimer < 15000) {
        setLoginTimer(Date.now())
      } else if (Date.now() - loginTimer >= 15000) {
          dispatch(logout())
      }
    }
  }


  return (
    <SomeComponent />
    ...
  ) 

}

I am almost certain this is a pretty basic error I am running into because my understanding of how this is meant to be handled is not yet well formed enough.

Thanks in advance.

The problem is coming from your reference to useState data in the callback function handleAppStateChange() . The callback for AppState.addEventListener is registered when the component mounts, but a snapshot of the current state is used for that callback each time it is called, and therefore, it will not be reactive in the way you would like.

The good news is that there are a number of ways to fix this issue, and some of those are explained in nice detail in this great answer here .

My solution, using information from Jacob K's link. It was very simple in the end and I got it down to an even more succinct statement. Can probably use a ternary, but this works just fine.


const rootNavigator = () => {

  const dispatch = useDispatch()
  const timer = useRef(Date.now())

  useEffect(() => {
    AppState.addEventListener('change', handleAppStateChange)

    return (() => {
      AppState.removeEventListener('change', handleAppStateChange)
    })
  }, [])

  const handleAppStateChange = (state) => {
    if (state === "inactive" || state === "background") {
      timer.current = Date.now()
    }
    if (state === "active" && (Date.now() - timer.current > 15000)) {
      dispatch(logout())
    }
  }


  return (
    <SomeComponent />
    ...
  ) 

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM