material-ui
material-ui copied to clipboard
Add useBreakpointValue hook
- [x] I have searched the issues of this repository and believe that this is not a duplicate.
Summary 💡
Provide a JavaScript API as a counterpart to the CSS sx breakpoint's API.
Examples 🌈
function MyApp() {
const elevation = useBreakpointValue({ xs: 1, md: 2 });
return <AppBar elevation={elevation} sx={{ mb: { xs: 1, md: 2 } }}>...</AppBar>;
}
Internally, useBreakpointValue would use useMediaQuery or useBreakpoint (probably better).
Motivation 🔦
We have started to surface the need for it in https://github.com/mui-org/material-ui/issues/15561#issuecomment-674454140. The hook can be used anytime a value needs to be determined based on the breakpoints. This comment https://github.com/mui-org/material-ui/issues/17000#issuecomment-740159615 triggered the idea around opening this issue. I think that this hook is best for props that are already dependent on JavaScript logic.
One could argue that many of the CSS properties could be made responsive, e.g. the color prop. However, it might be overkill, we could use this hook as an escape hatch.
We have been discussing the idea to remove the <HiddenCss> helper as the Box covers most of the usage thanks to the sx's display feature. I think that this JS counterpart can solve the <HiddenJs> part of the migration issue: https://github.com/mui-org/material-ui/issues/19704#issuecomment-612034247.
Benchmarks
- https://seek-oss.github.io/braid-design-system/components/useResponsiveValue
- https://react-restart.github.io/hooks/api/useBreakpoint
- https://chakra-ui.com/docs/hooks/use-breakpoint-value
- https://xstyled.dev/docs/hooks/#usebreakpoint
this will be really useful!
I guess useBreakpointValue will be listening for mediaQueryChanges and return the current value dictated by the props depending on what's the current breakPoint?
@atnpcg Regarding the implementation, we have been using useMediaQuery in https://github.com/mui-org/material-ui/blob/cef32a188d799618b4e0b1fe7b5ef201dbc85d6d/packages/material-ui/src/withWidth/withWidth.js#L54
instead of using window.innerWidth. However, to be honest, I'm not sure that the solution is great. We had reports about the logic triggering more rerenders that necessary, that it's harder to unit test, and that if the theme breakpoint structure change, it's not strict mode friendly.
It feels like it would be more efficient to rollback, leverage window.innerWidth. Maybe we could have an internal useBreakpoint() hook that returns the current breakpoint, and leverages it inside useBreakpointValue.
This API might help: https://github.com/mui-org/material-ui/blob/830c18ba71af19bc0370f1eeb902f9f605144a5d/packages/material-ui/src/Stack/Stack.js#L34
What are the use cases for this hook? I don't consider the initial primer a valid use case. Why would you want to increase elevation depending on the breakpoint? That's not what you should use elevation for.
What are the use cases for this hook?
The use case is to bridge the gap between any JavaScript behavior and the CSS utility breakpoint API.
Regarding the example, it was meant to demonstrate the API proposal. I think that the specific example (AppBar elevation) is irrelevant to the motivation for introducing the API. I have used it because it was raised specifically by this user: https://github.com/mui-org/material-ui/issues/15561#issuecomment-674317587. It's likely a design requirement for his project. Maybe a better example would have been:
function MyApp() {
const variant = useBreakpointValue({ xs: 'temporary', md: 'permanent' });
return <Drawer variant={variant}>;
}
The use case is to bridge the gap between any JavaScript behavior and the CSS utility breakpoint API.
That's not a use case. It's the exact opposite of a use case because it's obvious that a solution already exist in the form of CSS. Why this needs to be transferred to JS is something you should be able to provide.
(AppBar elevation) is irrelevant to the motivation for introducing the API
A motivation for a solution is absolutely relevant. There's no point in working on a problem if you're unable to describe the problem at a higher level. Code should not be written just to exist.
What are the use cases for this hook? I don't consider the initial primer a valid use case. Why would you want to increase elevation depending on the breakpoint? That's not what you should use elevation for.
What about changing Tabs , AppBar orientation from horizontal to vertical depending on the breakpoint.
This is something Chakra supports out of the box.
The general point here is that currently in MUI it is difficult to make some values responsive, https://github.com/mui-org/material-ui/issues/6140 was a step in the right direction, but it only applies to the Grid component.
Anyway heres a quick one I created for my own use - has not been tested in production to any great extent.
export const useBreakpointValue = <TValue>(
values: {
[key in Breakpoint]?: TValue;
},
) => {
const matches = {
xs: useMediaQuery(theme.breakpoints.up(`xs`)),
sm: useMediaQuery(theme.breakpoints.up(`sm`)),
md: useMediaQuery(theme.breakpoints.up(`md`)),
lg: useMediaQuery(theme.breakpoints.up(`lg`)),
xl: useMediaQuery(theme.breakpoints.up(`xl`)),
};
const validBreakpoints = Object.entries(matches)
.filter(
([breakpoint, isMatch]) =>
Object.keys(values).includes(breakpoint) && isMatch,
)
.map(([key]) => key);
const largestBreakpoint = validBreakpoints.pop();
if (!largestBreakpoint) {
return values[0];
}
return values[largestBreakpoint];
};
I had the same use case of changing component properties based on the current breakpoint.
To get the largest matching breakpoint, I use the following:
import { useMediaQuery } from "@mui/material";
export const useBreakpoint = () => {
const xs = useMediaQuery(theme => theme.breakpoints.up('xs'));
const sm = useMediaQuery(theme => theme.breakpoints.up('sm'));
const md = useMediaQuery(theme => theme.breakpoints.up('md'));
const lg = useMediaQuery(theme => theme.breakpoints.up('lg'));
const xl = useMediaQuery(theme => theme.breakpoints.up('xl'));
switch (true) {
case xl: return 'xl'
case lg: return 'lg'
case md: return 'md'
case sm: return 'sm'
case xs: return 'xs'
default: return 'xxs'
}
}
What are the use cases for this hook? I don't consider the initial primer a valid use case. Why would you want to increase elevation depending on the breakpoint? That's not what you should use elevation for.
What about changing
Tabs,AppBarorientation fromhorizontaltoverticaldepending on the breakpoint.This is something Chakra supports out of the box.
The general point here is that currently in MUI it is difficult to make some values responsive, #6140 was a step in the right direction, but it only applies to the
Gridcomponent.Anyway heres a quick one I created for my own use - has not been tested in production to any great extent.
export const useBreakpointValue = <TValue>( values: { [key in Breakpoint]?: TValue; }, ) => { const matches = { xs: useMediaQuery(theme.breakpoints.up(`xs`)), sm: useMediaQuery(theme.breakpoints.up(`sm`)), md: useMediaQuery(theme.breakpoints.up(`md`)), lg: useMediaQuery(theme.breakpoints.up(`lg`)), xl: useMediaQuery(theme.breakpoints.up(`xl`)), }; const validBreakpoints = Object.entries(matches) .filter( ([breakpoint, isMatch]) => Object.keys(values).includes(breakpoint) && isMatch, ) .map(([key]) => key); const largestBreakpoint = validBreakpoints.pop(); if (!largestBreakpoint) { return values[0]; } return values[largestBreakpoint]; };
The problem with this approach is that useMediaQuery hook is called 6 times on each rendering.
Would be awesome if there was a way to listen to some property to only call when needed (so we could add debounce to it).
@caio-borghi-yapi yes you are right, and I have actually noticed performance issues with it.
so I would not recommend my hook for production
Do you have any ideia if there is a good way to set the currentBreakpoint without causing many rerenders?
A property from useTheme to listen to would be great.
@oliviertassinari could this one be re-opened?
Btw, it would also be great to have a basic hook which does not return a value from an object map, but just the breakpoint value as string and/or index:
export function useBreakpoint(): string; // xs | sm | md | ....
export function useBreakpointIndex(): number; // 0 | 1 | ....
Do you have any ideia if there is a good way to set the currentBreakpoint without causing many rerenders?
I couldn't find anything in MUI to help, so wrote my own and used a 3rd party lib to ensure there aren't going to be x number of instances and listeners. Also mapped over the breakpoints (instead of hard coding them) as the number of breakpoints could change per project.
import { useTheme } from "@mui/material";
import { Breakpoint } from "@mui/system";
import { useEffect, useState } from "react";
import { singletonHook } from "react-singleton-hook";
// https://github.com/Light-Keeper/react-singleton-hook/issues/406#issuecomment-962282765
// eslint-disable-next-line no-underscore-dangle
export function _useCurrentBreakpoint(): Breakpoint {
const globalTheme = useTheme();
const mqs: [Breakpoint, string][] = globalTheme.breakpoints.keys.map(
(key, index, breakpoints) => {
let mq = "";
if (index === breakpoints.length - 1) {
mq = globalTheme.breakpoints.up(key);
} else {
mq = globalTheme.breakpoints.between(key, breakpoints[index + 1]);
}
return [key, mq.replace(/^@media( ?)/m, "")];
}
);
const [currentBreakpoint, setCurrentBreakpoint] = useState<Breakpoint>(() => {
const bp = mqs.find(([, mq]) => window.matchMedia(mq).matches);
return bp ? bp[0] : "xs";
});
useEffect(() => {
function handleCurrentBreakpointChange(
key: Breakpoint,
e: MediaQueryListEvent
) {
if (e.matches) {
setCurrentBreakpoint(key);
}
}
const handlers: [string, (e: MediaQueryListEvent) => void][] = mqs.map(
([key, mq]) => {
const handler = (e: MediaQueryListEvent) =>
handleCurrentBreakpointChange(key, e);
return [mq, handler];
}
);
handlers.forEach(([mq, handler]) => {
window.matchMedia(mq).addEventListener("change", handler);
});
return () => {
handlers.forEach(([mq, handler]) => {
window.matchMedia(mq).removeEventListener("change", handler);
});
};
}, [mqs]);
return currentBreakpoint;
}
const useCurrentBreakpoint = singletonHook("xs", _useCurrentBreakpoint);
export { useCurrentBreakpoint };