index.js:1 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.
import { breakpoints } from "helpers/breakpoints"
import { useWindowSize } from "hooks/useWindowSize"
import { useEffect, useState } from "react"
import { Viewport } from "./types/useViewportTypes"
const useViewport = () => {
const [viewport, setViewport] = useState<Viewport | undefined>(undefined)
const windowWidth: number | undefined = useWindowSize().width
useEffect(() => {
let vp: Viewport | undefined = undefined
const width = windowWidth ? windowWidth : window.innerWidth
if (width) {
const isZeroAndUp = width >= 0
const isMobileAndUp = width >= breakpoints.mobileAndUp
const isTabletAndUp = width >= breakpoints.tabletAndUp
const isDesktopAndUp = width >= breakpoints.desktopAndUp
const isLargeDesktopAndUp = width >= breakpoints.largeDesktopAndUp
const isMaxWidthAndUp = width >= breakpoints.maxWidthAndUp
vp = {
isZeroAndUp,
isMobileAndUp,
isTabletAndUp,
isDesktopAndUp,
isLargeDesktopAndUp,
isMaxWidthAndUp,
}
}
setViewport(vp)
}, [windowWidth])
return viewport
}
export default useViewport
ViewPort is also using more hooks to check for window size.
import { useState, useEffect } from "react"
import { debounce } from "helpers/helpers"
// Hook
export function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
})
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
const vh = (window.top || window).innerHeight * 0.01
document.documentElement.style.setProperty("--vh", `${vh}px`)
}
const debouncedHandleResize = debounce(handleResize, 300)
// Add event listener
window.addEventListener("resize", debouncedHandleResize)
// Call handler right away so state gets updated with initial window size
debouncedHandleResize()
// Remove event listener on cleanup
return () => window.removeEventListener("resize", debouncedHandleResize)
}, [])
return windowSize
}
And the debounce function.
// Debounce function
export function debounce<U = unknown, V = void>(
fn: (args: U) => V,
wait: number
) {
let timer: ReturnType<typeof setTimeout>
return (args: U): Promise<V> =>
new Promise((resolve) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => resolve(fn(args)), wait)
})
}
The issue is that with a debounced or otherwise deferred function, it's possible for setViewport
(or any other state setter) to get called after the component itself is gone (and doing setViewport
will not affect anything, as there could be nothing to read that state).
That's why React is telling you there could be a memory leak in your app. In practice, in this case it doesn't really matter; the debounced/deferred call is an one-shot.
If you really want to quiesce the warning, then you'd need something like an useIsMounted
hook:
function useIsMounted() {
const mountedRef = useRef(false);
const get = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => mountedRef.current = false;
}, []);
return get;
}
// ...
function MyComponent() {
const isMounted = useIsMounted();
// ... in your handler...
if(isMounted()) setViewport(...);
}
so you don't call the setter if the component has already unmounted.