use-media
useMedia
React sensor hook that tracks state of a CSS media query.
Usage
With useEffect
import useMedia from 'use-media';
const Demo = () => {
const isWide = useMedia({minWidth: 1000});
const reduceMotion = useMedia('(prefers-reduced-motion: reduce)');
return (
<div>
Screen is wide: {isWide ? '😃' : '😢'}
</div>
);
};
With useLayoutEffect
import {useMediaLayout} from 'use-media';
const Demo = () => {
const isWide = useMediaLayout({minWidth: 1000});
const reduceMotion = useMediaLayout('(prefers-reduced-motion: reduce)');
return (
<div>
Screen is wide: {isWide ? '😃' : '😢'}
</div>
);
};
Testing
Depending on your testing setup, you may need to mock window.matchMedia
on components that utilize the useMedia
hook. Below is an example of doing this in jest
:
/test-utilities/index.ts
import {mockMediaQueryList} from 'use-media/lib/useMedia';
export interface MockMatchMedia {
media: string;
matches?: boolean;
}
function getMockImplementation({media, matches = false}: MockMatchMedia) {
const mql: MediaQueryList = {
...mockMediaQueryList,
media,
matches,
};
return () => mql;
}
export function jestMockMatchMedia({media, matches = false}: MockMatchMedia) {
const mockedImplementation = getMockImplementation({media, matches});
window.matchMedia = jest.fn().mockImplementation(mockedImplementation);
}
/components/MyComponent/MyComponent.test.tsx
const mediaQueries = {
mobile: '(max-width: 767px)',
prefersReducedMotion: '(prefers-reduced-motion: reduce)',
};
describe('<MyComponent />', () => {
const defaultProps: Props = {
duration: 100,
};
afterEach(() => {
jestMockMatchMedia({
media: mediaQueries.prefersReducedMotion,
matches: false,
});
});
it('sets `duration` to `0` when user-agent `prefers-reduced-motion`', () => {
jestMockMatchMedia({
media: mediaQueries.prefersReducedMotion,
matches: true,
});
const wrapper = mount(<MyComponent {...defaultProps} />);
const child = wrapper.find(TransitionComponent);
expect(child.prop('duration')).toBe(0);
});
});
Storing in Context
Depending on your app, you may be using the useMedia
hook to register many matchMedia
listeners across multiple components. It may help to elevate these listeners to Context
.
/components/MediaQueryProvider/MediaQueryProvider.tsx
import React, {createContext, useContext, useMemo} from 'react';
import useMedia from 'use-media';
interface Props {
children: React.ReactNode;
}
export const MediaQueryContext = createContext(null);
const mediaQueries = {
mobile: '(max-width: 767px)',
prefersReducedMotion: '(prefers-reduced-motion: reduce)',
};
export default function MediaQueryProvider({children}: Props) {
const mobileView = useMedia(mediaQueries.mobile);
const prefersReducedMotion = useMedia(mediaQueries.prefersReducedMotion);
const value = useMemo(() => ({mobileView, prefersReducedMotion}), [
mobileView,
prefersReducedMotion,
]);
return (
<MediaQueryContext.Provider value={value}>
{children}
</MediaQueryContext.Provider>
);
}
export function useMediaQueryContext() {
return useContext(MediaQueryContext);
}
/components/App/App.tsx
import React from 'react';
import MediaQueryProvider from '../MediaQueryProvider';
import MyComponent from '../MyComponent';
export default function App() {
return (
<MediaQueryProvider>
<div id="MyApp">
<MyComponent />
</div>
</MediaQueryProvider>
);
}
/components/MyComponent/MyComponent.tsx
import React from 'react';
import {useMediaQueryContext} from '../MediaQueryProvider';
export default function MyComponent() {
const {mobileView, prefersReducedMotion} = useMediaQueryContext();
return (
<div>
<p>mobileView: {Boolean(mobileView).toString()}</p>
<p>prefersReducedMotion: {Boolean(prefersReducedMotion).toString()}</p>
</div>
);
}