custom hooks

useMatchMedia( )

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Explanation

The useMatchMedia hook simplifies responsive design implementation in React applications by streamlining media query management. Developers can define device-specific conditions with ease, abstracting away the complexities of directly working with window.matchMedia. This hook offers a straightforward interface for accessing media query results, enhancing code readability and maintainability.

By integrating useMatchMedia, developers can encapsulate media query logic within reusable components, fostering modular and scalable code structures. This enables efficient handling of different device sizes without cluttering component code with intricate conditions. With its intuitive API, useMatchMedia facilitates the creation of responsive designs, empowering developers to focus on crafting engaging user interfaces.

Furthermore, useMatchMedia promotes best practices by encouraging separation of concerns and encapsulation of functionality. Centralizing media query management within this custom hook promotes code reusability and simplifies maintenance efforts. This abstraction shields developers from the complexities of directly interacting with MediaQueryList objects, providing an ergonomic solution for responsive behavior in React components.

Developers can easily customize device-specific conditions according to their project requirements by modifying the devices object within the hook. By adjusting the key-value pairs in the devices object, developers can define their own breakpoints and media query conditions tailored to their application's design and layout needs.

In essence, useMatchMedia offers a streamlined approach to responsive design implementation, abstracting away the intricacies of media query handling while providing a clean and intuitive interface. Its integration within React components fosters code modularity, readability, and scalability, ultimately enhancing the development experience and delivering a seamless user experience across various devices.

Usage Cases

  • Adaptive Layouts: Create responsive layouts that adapt seamlessly to various device sizes, ensuring optimal display across mobile, tablet, and desktop screens.
  • Conditional Component Rendering: Implement conditional rendering based on device type, allowing different components to be displayed for mobile, tablet, and desktop users, enhancing user experience and content presentation.
  • Dynamic Styling: Adjust CSS styles dynamically based on the user's device, enabling tailored visual experiences for different screen sizes and resolutions without cluttering code with manual media query management.
  • Device-Specific Functionality: Tailor functionality within components to specific devices, providing unique features or interactions optimized for mobile, tablet, and desktop users, enhancing usability and engagement.
  • Media Query Abstraction: Abstract away the complexities of media query handling by encapsulating logic within the custom hook, promoting code reusability and simplifying maintenance efforts in responsive web development.
  • Efficient Resource Loading: Optimize resource loading by conditionally fetching or rendering content based on device characteristics determined by the custom hook, improving performance and reducing unnecessary bandwidth usage.

Creation

Code implementation
1// Define an object for different device types, each associated with a media
2// query string that describes the screen size ranges for those devices
3const devices = {
4 isMobile: '(max-width: 766px)',
5 isTablet: '(min-width: 767px) and (max-width: 1199px)',
6 isDesktop: '(min-width: 1200px)',
7};
8
9// Create an array by mapping over the values of the `devices` object,
10// to generate a `MediaQueryList` object for each media query string.
11// These objects are used to check if the document matches the media query
12const mediaQueryLists = Object.values(devices).map((query) => {
13 return window.matchMedia(query);
14});
15
16// Function to get an array of boolean values by mapping over `mediaQueryLists`,
17// where each boolean indicates whether the respective media query
18// currently matches the document state
19const getValues = (): boolean[] => {
20 return mediaQueryLists.map((list: MediaQueryList) => list.matches);
21};
22
23// Define the return type of the hook.
24// The keys represent device types, and the values indicate the match status for each device type
25type MatchMediaResult = Record<string, boolean>;
26
27const useMatchMedia = (): MatchMediaResult => {
28 const [values, setValues] = useState<boolean[]>(getValues);
29
30 useLayoutEffect(() => {
31 // Define the handler function that updates the state when the status of a media query changes
32 const handleChange = () => {
33 setValues(getValues());
34 };
35
36 // Attach an event listener for the `change` event to each `MediaQueryList`
37 // in `mediaQueryLists`, so that `handleChange` is called whenever the match
38 // status of a media query changes
39 mediaQueryLists.forEach((list) =>
40 list.addEventListener('change', handleChange)
41 );
42
43 // Return a cleanup function that removes the event listeners
44 // from each `MediaQueryList` when the component unmounts,
45 // ensuring there are no duplicate listeners
46 return () =>
47 mediaQueryLists.forEach((list) =>
48 list.removeEventListener('change', handleChange)
49 );
50 }, []);
51
52 // Create an object by reducing over the keys of devices,
53 // mapping each device type to its corresponding boolean value
54 // from the values array, which represents the match status
55 const deviceIndicators: MatchMediaResult = Object.keys(devices).reduce(
56 (acc, screen, index) => ({
57 ...acc,
58 [screen]: values[index],
59 }),
60 {}
61 );
62
63 // Return the object containing device indicators,
64 // which provide a boolean value for each device type indicating
65 // whether the current screen size matches the corresponding media query
66 return deviceIndicators;
67};
68
69export default useMatchMedia;
70

Reference

Code implementation
1const { isMobile, isTablet, isDesktop } = useMatchMedia();
2

Parameters

This hook doesn't accept any parameters.

Return Values

NameTypeDescription
isMobilebooleanA boolean value indicating whether the current device is recognized as a mobile device.
isTabletbooleanA boolean value indicating whether the current device is recognized as a tablet device.
isDesktopbooleanA boolean value indicating whether the current device is recognized as a desktop device.

Example Usages

Dynamic Component Rendering Based on Device Type

This example demonstrates how to use the useMatchMedia custom hook to create a responsive layout that adapts based on the device type. The hook returns values indicating whether the user is on a mobile, tablet, or desktop device. Based on this, different components are rendered for each device type, ensuring an optimized user experience across various screen sizes. This approach allows the UI to dynamically adjust and provide the most suitable layout for each device.

Code implementation
1const Container: FC = () => {
2 // Destructure the values returned by the custom hook
3 const { isMobile, isTablet } = useMatchMedia();
4
5 return (
6 <Layer title="Container">
7 {
8 // Render different components based on the device type
9 isMobile ? (
10 // If the device is mobile, render the `MobileComponent`
11 <MobileComponent />
12 ) : // If the device is not mobile, check if it's a tablet
13 isTablet ? (
14 // If it's, render the `TabletComponent`,
15 <TabletComponent />
16 ) : (
17 // otherwise, render the `DesktopComponent`
18 <DesktopComponent />
19 )
20 }
21 </Layer>
22 );
23};
24
An interactive output of the code above

Adaptive Menu Based on Device Type

This example demonstrates how to conditionally render different menu components based on the device type using the useMatchMedia custom hook. When viewed on a mobile device, the HamburgerMenu is displayed, providing a compact navigation option. On larger screens, the RegularMenu is shown, offering a more traditional navigation layout. This approach ensures an optimized user experience across various devices.

Code implementation
1const Container: FC = () => {
2 const { isMobile } = useMatchMedia();
3
4 return (
5 <Layer title="Container">
6 {
7 // Conditional rendering of menu component based on device type
8 isMobile ? <HamburgerMenu /> : <RegularMenu />
9 }
10 </Layer>
11 );
12};
13
An interactive output of the code above

Responsive Grid Layout with Dynamic Device Adaptation

This example demonstrates a responsive grid layout that adapts to different device types. Using the useMatchMedia custom hook, the layout dynamically adjusts the number of columns based on whether the user is on a mobile, tablet, or desktop device. The class for the container is dynamically set with the containerClass variable, which ensures that the appropriate grid styles are applied based on the device type. This ensures an optimized user experience across various screen sizes, making the interface more intuitive and user-friendly.

Code implementation
1.container {
2 /* Styles for mobile devices */
3 /* Blocks are stacked vertically */
4 display: grid;
5 grid-template-columns: repeat(1, 1fr);
6 gap: 8px;
7 max-height: 320px;
8 overflow-y: auto;
9 padding: 16px 0;
10 width: 100%;
11}
12
13.tablet {
14 /* Additional styles for tablet devices */
15 /* Blocks are arranged in a 2-column grid */
16 grid-template-columns: repeat(2, 1fr);
17}
18
19.desktop {
20 /* Additional styles for desktop devices */
21 /* Blocks are arranged in a 3-column grid */
22 grid-template-columns: repeat(3, 1fr);
23}
24
Code implementation
1const items = loadData(6);
2
3const Container: FC = () => {
4 const { isTablet, isDesktop } = useMatchMedia();
5
6 // Determine the container class based on the device type.
7 // If the device is a desktop, set the container class to "desktop".
8 // If it's a tablet, set the container class to "tablet".
9 // otherwise (if it's a mobile device) keep it as "container".
10 const containerClass = isDesktop ? 'desktop' : isTablet ? 'tablet' : '';
11
12 return (
13 <Layer title="Container">
14 <Message>
15 Dynamic styling adapted to your device. Block positions adjust
16 seamlessly based on screen size.
17 </Message>
18 <div className="relative border-y-4 border-neutral-900 dark:border-neutral-100">
19 <div className={cn('container', containerClass)}>
20 {items.map((item) => (
21 <Square key={item.id}>
22 <span className="font-monoBrand p-4 text-center text-2xl">
23 {item.name}
24 </span>
25 </Square>
26 ))}
27 </div>
28 </div>
29 </Layer>
30 );
31};
32
An interactive output of the code above
Last updated on by @skrykateSuggest an improvement on Github repository.