How to setInterval for every 5 second render with React hook useEffect in React Native app?
Asked Answered
S

3

33

I have React Native app and I get data from API by fetch. I created custom hook that get data from API. And i need to re-render it every 5 seconds. For it I wrapped my custom hook to setInterval and after my app become work very slowly and when I navigate to another screen I get this error:

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Can you tell me please how can I solve this bug and which will be the best way to setInterval, because I think my way is not good.

My custom hook:

export const useFetch = url => {
  const [state, setState] = useState({ data: null, error: false, loading: true })

  useEffect(() => {
    setInterval(() => {
      setState(state => ({ data: state.data, error: false, loading: true }))
      fetch(url)
        .then(data => data.json())
        .then(obj =>
          Object.keys(obj).map(key => {
            let newData = obj[key]
            newData.key = key
            return newData
          })
        )
        .then(newData => setState({ data: newData, error: false, loading: false }))
        .catch(function(error) {
          console.log(error)
          setState({ data: null, error: true, loading: false })
        })
    }, 5000)
  }, [url, useState])
  useEffect(() => () => console.log('unmount'), [])
  return state
}

My Component:

const ChartsScreen = ({ navigation }) => {
  const { container } = styles
  const url = 'https://poloniex.com/public?command=returnTicker'
  const { data, error, loading } = useFetch(url)

  const percentColorHandler = number => {
    return number >= 0 ? true : false
  }

  return (
    <View style={container}>
      <ProjectStatusBar />
      <IconsHeader
        dataError={false}
        header="Charts"
        leftIconName="ios-arrow-back"
        leftIconPress={() => navigation.navigate('Welcome')}
      />
      <ChartsHeader />
      <ActivityIndicator animating={loading} color="#068485" style={{ top: HP('30%') }} size="small" />
      <FlatList
        data={data}
        keyExtractor={item => item.key}
        renderItem={({ item }) => (
          <CryptoItem
            name={item.key}
            highBid={item.highestBid}
            lastBid={item.last}
            percent={item.percentChange}
            percentColor={percentColorHandler(item.percentChange)}
          />
        )}
      />
    </View>
  )
}

Stephanistephania answered 18/8, 2019 at 6:41 Comment(0)
P
70

You need to clear your interval,

useEffect(() => {
  const intervalId = setInterval(() => {  //assign interval to a variable to clear it.
    setState(state => ({ data: state.data, error: false, loading: true }))
    fetch(url)
      .then(data => data.json())
      .then(obj =>
        Object.keys(obj).map(key => {
          let newData = obj[key]
          newData.key = key
          return newData
        })
     )
     .then(newData => setState({ data: newData, error: false, loading: false }))
     .catch(function(error) {
        console.log(error)
        setState({ data: null, error: true, loading: false })
     })
  }, 5000)

  return () => clearInterval(intervalId); //This is important
 
}, [url, useState])

For more about cleanup functions in useEffect refer to this.

Precentor answered 18/8, 2019 at 6:53 Comment(9)
I add this 2 lines, but anyway has an error and app work very slowly.Stephanistephania
What is error? And app work slow because of setInterval time, every time it will wait for 5 sec. Try to reduce the time.Precentor
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.Stephanistephania
Do you have another setInterval in your app? By doing this return () => clearInterval(intervalId); it will clear setInterval.Precentor
No, i don't have another setInterval, just one. And when the component is unmount the render is still happening.Stephanistephania
Why do you have this useEffect(() => () => console.log('unmount'), [])? Try to remove this.Precentor
Because i thought to use for unmounting of the component. I deleted it and now it works better. Thank you so much. Please can tell me one more thing, this component render in the one tab of my tabsNavigator and when i change tab component is not unmounting, the render is still working. Only when i leave the navigation stack the component is unmounting. Why? And the error is still appearing but not so often like beforeStephanistephania
This is because when tabs are loaded all the component are rendered, so when you switch tabs previous tab component are not unmounted. You can conditionally render component on every tab change. Means you can add a condition {activeTab==="tab_name" && <Component />} something like this.Precentor
work like a charm using dispatch with react-redux, thanks :)Martguerita
E
6

For React Hooks + Apollo to fetch data from a GraphQL server every 5 seconds. In this example, we logout the user in React if the user is not logged-in in the backend. (JWT token not valide anymore)

import React from 'react'
import gql from 'graphql-tag'
import { useApolloClient } from '@apollo/react-hooks'

export const QUERY = gql`
  query Me {
    me {
      id
    }
  }
`

const MyIdle = () => {
  const client = useApolloClient()

  React.useEffect(() => {
    async function fetchMyAPI() {
      try {
        await client.query({
          query: QUERY,
          fetchPolicy: 'no-cache',
        })
      } catch (e) {
        // Logout the user and redirect to the login page
      }
    }

    const intervalId = setInterval(() => {
      fetchMyAPI()
    }, 1000 * 5) // in milliseconds
    return () => clearInterval(intervalId)
  }, [client])

  return null
}
export default MyIdle

Exuberance answered 21/4, 2021 at 20:55 Comment(0)
G
3

It might be both these things:

  • You need to clear up your interval
  • You need to not update state from your API callback if its unmounted.

Code:

useEffect(() => {
   let isMounted = true
   const intervalId = setInterval(() => {  //assign interval to a variaable to clear it
    setState(state => ({ data: state.data, error: false, loading: true }))
    fetch(url)
      .then(data => data.json())
      .then(obj =>
        Object.keys(obj).map(key => {
          let newData = obj[key]
          newData.key = key
          return newData
        })
     )
     .then(newData => {
        if(!isMounted) return  // This will cancel the setState when unmounted
        setState({ data: newData, error: false, loading: false })
     })
     .catch(function(error) {
        console.log(error)
        setState({ data: null, error: true, loading: false })
     })
   }, 5000)

   return () => {
       clearInterval(intervalId); //This is important
       isMounted = false // Let's us know the component is no longer mounted.
   }

}, [url, useState])

Might want, depending on your server response time, add a failsafe for pending queries (example, if you sent out a query and the next one launches before the first one returns...).

Gupton answered 15/4, 2020 at 19:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.