useMediaQuery not working -- using stale state value
See original GitHub issueDescription
When I resize the screen to be smaller, then larger, it continues to think itās small
Link to Reproduction
š„²
Description of the bug
Weāre comparing matches
to currentMatches
, this is where the bug is. Because we do not supply matches
as a dependency for the useSafeLayoutEffects
in https://github.com/chakra-ui/chakra-ui/blob/a0de717c98e574e73f8a444f576fc738907230ab/packages/media-query/src/use-media-query.ts#L53
So we keep a reference to a function that has a stale reference to matches, therefore it keeps using the original value instead of the latest one.
Thereās two ways to fix it:
- Simply add
matches
to the dependencies array for theuseSafeLayoutEffect
hook - Use a ref as a secondary storage measure to store
matches
and use that in comparison,useRef
returns a stable value, so we do not need to add it to the dependencies.
I chose the latter in my patch because I did not read about useSafeLayoutEffect
and do not know what excessive re-cals in it will cause
Patch
Hereās the patched one Iām using, the only change is the use of useRef
import { useEnvironment } from '@chakra-ui/react-env'
import { isBrowser } from '@chakra-ui/utils'
import * as React from 'react'
const useSafeLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect
// NOTE: This hook is a PATCHED version of the "useMediaQuery" hook
// that ships with @chakra-ui/react. The difference/fix we have is that
// we're storing the "matches" value in a ref for comparison, therefore
// fixing a bug by not using stale value generated at first-run time.
/**
* React hook that tracks state of a CSS media query
*
* @param query the media query to match
*/
export default function useMediaQuery(query: string | string[]): boolean[] {
const env = useEnvironment()
const queries = Array.isArray(query) ? query : [query]
const isSupported = isBrowser && 'matchMedia' in env.window
const [matches, setMatches] = React.useState(
queries.map(queryItem => (isSupported ? !!env.window.matchMedia(queryItem).matches : false))
)
const matchesRef = React.useRef(matches)
useSafeLayoutEffect(() => {
if (!isSupported) return undefined
const mediaQueryList = queries.map(queryItem => env.window.matchMedia(queryItem))
const listenerList = mediaQueryList.map(() => {
const listener = () => {
const isEqual = (prev: boolean[], curr: boolean[]) =>
prev.length === curr.length && prev.every((elem, idx) => elem === curr[idx])
const currentMatches = mediaQueryList.map(mediaQuery => mediaQuery.matches)
if (!isEqual(matchesRef.current, currentMatches)) {
matchesRef.current = currentMatches
setMatches(currentMatches)
}
}
env.window.addEventListener('resize', listener)
return listener
})
return () => {
mediaQueryList.forEach((_, index) => {
env.window.removeEventListener('resize', listenerList[index])
})
}
}, [query])
return matches
}
Chakra UI Version
1.7.1
Browser
Google Chrome 95
Operating System
- macOS
- Windows
- Linux
Additional Information
Let me know if I can explain this further, hope the patch is helpful!
Issue Analytics
- State:
- Created 2 years ago
- Reactions:2
- Comments:10 (5 by maintainers)
Top GitHub Comments
Hi @segunadebayo is this fixed in the latest version? Sorry I couldnāt find it in the release notes
Thanks. It should be
mediaQueryList.removeEventListener
. I spent the weekend going over the code and creating a mock so that it could have some unit tests. What Iāll be dropping today should be more performant. The listener will use the MQL event generated so that it only responds to actual changes rather than window resizing. I also removed the useEffect dependency list as thereās no need for it with my changes. Having a dependency caused the listeners to unload and reload. That cause events to be missed. Thereās no need for unloading unless the component using the hook unloads. Quite a few hours spent researching, writing and debugging.