Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useBreakpointValue hook #23885

Open
1 task done
oliviertassinari opened this issue Dec 7, 2020 · 15 comments
Open
1 task done

Add useBreakpointValue hook #23885

oliviertassinari opened this issue Dec 7, 2020 · 15 comments
Labels
new feature New feature or request priority: important This change can make a difference

Comments

@oliviertassinari
Copy link
Member

oliviertassinari commented Dec 7, 2020

  • 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 #15561 (comment). The hook can be used anytime a value needs to be determined based on the breakpoints. This comment #17000 (comment) 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: #19704 (comment).

Benchmarks

@oliviertassinari oliviertassinari added new feature New feature or request waiting for 👍 Waiting for upvotes labels Dec 7, 2020
@atnpcg
Copy link

atnpcg commented Dec 8, 2020

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?

@oliviertassinari
Copy link
Member Author

oliviertassinari commented Dec 8, 2020

@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.

@eps1lon
Copy link
Member

eps1lon commented Apr 8, 2021

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.

@oliviertassinari
Copy link
Member Author

oliviertassinari commented Apr 8, 2021

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: #15561 (comment). 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}>;
}

@eps1lon
Copy link
Member

eps1lon commented Apr 8, 2021

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.

@dan-cooke
Copy link

dan-cooke commented Dec 26, 2021

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, #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];
};

@montanaflynn
Copy link

I had the same use case of changing component properties based on the current breakpoint.

@gisodal
Copy link

gisodal commented Mar 8, 2022

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' 
  }
}

@caio-borghi-yapi
Copy link

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, #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];
};

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).

@dan-cooke
Copy link

@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

@caio-borghi-yapi
Copy link

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.

@caio-borghi-yapi

This comment was marked as off-topic.

@olee
Copy link
Contributor

olee commented Sep 20, 2022

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 | ....

@Zach-Jaensch
Copy link

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 };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature New feature or request priority: important This change can make a difference
Projects
None yet
Development

No branches or pull requests

9 participants