Skip to content

Instantly share code, notes, and snippets.

@maapteh
Last active January 28, 2022 18:44
Show Gist options
  • Save maapteh/9f11597e6f20862400699063d2ffe5a0 to your computer and use it in GitHub Desktop.
Save maapteh/9f11597e6f20862400699063d2ffe5a0 to your computer and use it in GitHub Desktop.
Viewport based on MatchMedia
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