28

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>
  )
}

ravibagul91
  • 20,072
  • 5
  • 36
  • 59
jocoders
  • 1,594
  • 2
  • 19
  • 54

3 Answers3

62

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.

Bruce Seymour
  • 1,520
  • 16
  • 24
ravibagul91
  • 20,072
  • 5
  • 36
  • 59
  • I add this 2 lines, but anyway has an error and app work very slowly. – jocoders Aug 18 '19 at 07:04
  • What is error? And app work slow because of setInterval time, every time it will wait for 5 sec. Try to reduce the time. – ravibagul91 Aug 18 '19 at 07:07
  • 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. – jocoders Aug 18 '19 at 07:10
  • 1
    Do you have another setInterval in your app? By doing this `return () => clearInterval(intervalId);` it will clear setInterval. – ravibagul91 Aug 18 '19 at 07:14
  • No, i don't have another setInterval, just one. And when the component is unmount the render is still happening. – jocoders Aug 18 '19 at 07:18
  • Why do you have this `useEffect(() => () => console.log('unmount'), [])`? Try to remove this. – ravibagul91 Aug 18 '19 at 07:18
  • 1
    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 before – jocoders Aug 18 '19 at 07:27
  • 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" && } something like this. – ravibagul91 Aug 18 '19 at 07:40
  • work like a charm using dispatch with react-redux, thanks :) – ncesar Apr 10 '21 at 18:08
4

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

Alan
  • 9,167
  • 4
  • 52
  • 70
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...).

denislexic
  • 10,786
  • 23
  • 84
  • 128