How To Prevent Memory Leaks in React Hooks

Learn how to prevent astronomical server costs by properly handling React data update in useEffectr

image

I am currently building out an app using Firebase and React and I was surprised to find out that I hit my 50k read limit for the day. I couldn’t imagine how in the hell that was possible since my app only runs locally my machine, and that I was the ONLY user.

When using useEffect, your component will run code on render and executes the code inside of it when mounted. The problem that I was having was related to my app remounting when the state object was changed.

Here is an example of a poor way to perform an asynchronous call to the Firebase server.

function Example() {
    const [vehicles, setVehicles] = useState([])
    const vehiclesRef = collection(db, 'vehicles')

    useEffect(() => {
        const getVehicles = async () => {
            const data = await getDocs(vehiclesRef)
            setVehicles(data.docs.map( doc => (
                        {
                            ...doc.data(), 
                            id:doc.id 
                        }
                    )
                )
            )
        }

        getVehicles()
    }, [vehicles])       
}

along with the error that I was getting.

Warning: 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.

Whenever you call a setState variable, it will tell the component to re-render and run the code again. This means that setVehicles() will continue to call which will cause and endless loop of re-rendering, which will cost you a lot money down the road.

Here is how I fixed it.

function Example() {
    const [vehicles, setVehicles] = useState([])
    const vehiclesRef = collection(db, 'vehicles')
		const mounted = useRef(false)

    useEffect(() => {
        mounted.current = true

				if(mounted.current){
	        const getVehicles = async () => {
	            const data = await getDocs(vehiclesRef)
	            
	            if(isMounted){
	                setVehicles(data.docs.map( doc => (
	                            {
	                                ...doc.data(), 
	                                id:doc.id 
	                            }
	                        )
	                    )
	                )
	            }
	        }
	        getVehicles()
				}

        return () => mounted.current = false
    }, [vehicles]) 
}

Fortunately, the solution is quite straightforward when using a boolean flag. isMounted will determine whether or not the setState code will run, which in this case is called setVehicles()

When is mounted is false, the code will not run until that value is set to true, when the component is mounted again.

Hope this clears things up! If you have React and programming topics you’d like for me to cover, email me at christopher.clemmons2020@gmail.com.

Want to start a project with me? Send a inquiry to hello@digyt.co or visit digyt.co/contact