Last active
January 28, 2022 18:44
-
-
Save maapteh/9f11597e6f20862400699063d2ffe5a0 to your computer and use it in GitHub Desktop.
Viewport based on MatchMedia
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
FC, | |
ReactNode, | |
useState, | |
useEffect, | |
createContext, | |
useContext, | |
useCallback, | |
} from 'react'; | |
import { Viewport } from '@lib/types'; | |
// external lib hook | |
function useDebounce<T>(value: T, delay: number): T { | |
const [debounceValue, setDebounceValue] = useState(value); | |
useEffect(() => { | |
const handler = setTimeout(() => { | |
setDebounceValue(value); | |
}, delay); | |
return () => { | |
clearTimeout(handler); | |
}; | |
}, [value, delay]); | |
return debounceValue; | |
} | |
// external theme breakpoints | |
const breakpoints = { | |
xs: '22.5em', | |
sm: '30em', | |
md: '48em', | |
lg: '62em', | |
xl: '80em', | |
}; | |
// actual part for the Provider | |
const defaultValue = {}; | |
const ViewportContext = createContext(defaultValue); | |
const getViewport = (window: Window): Viewport => { | |
if (window.matchMedia(`(min-width: ${breakpoints.xl})`).matches) { | |
return Viewport.xl; | |
} | |
if (window.matchMedia(`(min-width: ${breakpoints.lg})`).matches) { | |
return Viewport.lg; | |
} | |
if (window.matchMedia(`(min-width: ${breakpoints.md})`).matches) { | |
return Viewport.md; | |
} | |
if (window.matchMedia(`(min-width: ${breakpoints.sm})`).matches) { | |
return Viewport.sm; | |
} | |
if (window.matchMedia(`(min-width: 0em)`).matches) { | |
return Viewport.xs; | |
} | |
// can not determine using matchMedia... | |
return Viewport.xl; | |
}; | |
type TViewportProps = { | |
children: ReactNode; | |
// on the server-side Request we use the user agent to create a best educated guess for its Viewport using user agent and library | |
initialViewport?: Viewport.lg | Viewport.sm | Viewport.md; | |
}; | |
/** | |
* This ViewportProvider should only be set on app level, not per component. It adds a window resize event handler once so our hook will always get the latest viewport size without too much performance impact! | |
*/ | |
const ViewportProvider: FC<TViewportProps> = ({ | |
children, | |
initialViewport, | |
}) => { | |
const [viewport, setViewport] = useState<Viewport>( | |
initialViewport || Viewport.lg | |
); | |
const viewportDebounced = useDebounce(viewport, 200); | |
useEffect(() => { | |
const currentViewport = getViewport(window); | |
setViewport(currentViewport); | |
const calcInnerWidth = () => { | |
const newViewport = getViewport(window); | |
setViewport(newViewport); | |
}; | |
window.addEventListener('resize', calcInnerWidth); | |
return () => { | |
window.removeEventListener('resize', calcInnerWidth); | |
}; | |
// when we add viewport it will add again listeners which we don't want, so do NOT add this! | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, []); | |
return ( | |
<ViewportContext.Provider value={!viewportDebounced ? viewport : viewportDebounced}> | |
{children} | |
</ViewportContext.Provider> | |
); | |
}; | |
function useViewport() { | |
const context = useContext(ViewportContext); | |
if (context === defaultValue) { | |
throw new Error('useViewport is not used within a ViewportProvider'); | |
} | |
return context; | |
} | |
export { | |
/** can be used on any component level to get the latest Viewport */ | |
useViewport, | |
/** should only be set at application level */ | |
ViewportProvider, | |
/** to ease the usage when using the hook */ | |
Viewport, | |
/** never consume this directly, only exported for our mocked provider, see our mocks! */ | |
ViewportContext, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment