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

[useMediaQuery] Accept function as argument & more #16343

Merged
merged 7 commits into from
Jul 15, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/pages/components/portal/portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The children of the portal component will be appended to the `container` specifi

The component is used internally by the [`Modal`](/components/modal/) and [`Popper`](/components/popper/) components.
On the server, the content won't be rendered.
You have to wait for the client side hydration to see the children.
You have to wait for the client-side hydration to see the children.

## Simple Portal

Expand Down
25 changes: 18 additions & 7 deletions docs/src/pages/components/use-media-query/ServerSide.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import React from 'react';
import mediaQuery from 'css-mediaquery';
import { ThemeProvider } from '@material-ui/styles';
import useMediaQueryTheme from '@material-ui/core/useMediaQuery';
import useMediaQuery from '@material-ui/core/useMediaQuery';

function MyComponent() {
const matches = useMediaQueryTheme('@media (min-width:600px)');
const matches = useMediaQuery('(min-width:600px)');

return <span>{`@media (min-width:600px) matches: ${matches}`}</span>;
return <span>{`(min-width:600px) matches: ${matches}`}</span>;
}

export default function ServerSide() {
// Use https://github.com/ericf/css-mediaquery as ponyfill.
const ssrMatchMedia = query => ({
// Use https://github.com/ericf/css-mediaquery as ponyfill.
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
// For the sake of this demo, we are using a fixed value.
// In production, you can look into client-hint https://caniuse.com/#search=client%20hint
// or user-agent resolution.
//
// In production, you can leverage:
//
// - Client hints. You can ask the client to send your server its width.
// Be aware that this feature is not supported everywhere: https://caniuse.com/#search=client%20hint.
// - User-agent. You can parse the user agent of the client, then convert the data to a
// is mobile or is desktop variable, and finally, guess the most likely screen width of the client.
width: 800,
}),
});

return (
<ThemeProvider theme={{ props: { MuiUseMediaQuery: { ssrMatchMedia } } }}>
<ThemeProvider
theme={{
props: {
MuiUseMediaQuery: { ssrMatchMedia },
},
}}
>
<MyComponent />
</ThemeProvider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import useMediaQuery from '@material-ui/core/useMediaQuery';

export default function SimpleMediaQuery() {
const matches = useMediaQuery('(min-width:600px)');

return <span>{`(min-width:600px) matches: ${matches}`}</span>;
}
62 changes: 55 additions & 7 deletions docs/src/pages/components/use-media-query/use-media-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ Some of the key features:
- ⚛️ It has an idiomatic React API.
- 🚀 It's performant, it observes the document to detect when its media queries change, instead of polling the values periodically.
- 📦 [1 kB gzipped](/size-snapshot).
- 💄 It's an alternative to react-responsive and react-media that aims for simplicity.
- 🤖 It supports Server-side rendering.
- 🤖 It supports server-side rendering.

## Simple media query

Expand All @@ -39,19 +38,68 @@ function MyComponent() {

{{"demo": "pages/components/use-media-query/ThemeHelper.js"}}

Alternatively, you can use a callback function, accepting the theme as a first argument:

```jsx
import useMediaQuery from '@material-ui/core/useMediaQuery';

function MyComponent() {
const matches = useMediaQuery(theme => theme.breakpoints.up('sm'));

return <span>{`theme.breakpoints.up('sm') matches: ${matches}`}</span>;
}
```

⚠️ There is **no default** theme support, you have to inject it in a parent theme provider.

## Using JavaScript syntax

[json2mq](https://github.com/akiran/json2mq) is used to generate media query string from a JavaScript object.
You can use [json2mq](https://github.com/akiran/json2mq) to generate media query string from a JavaScript object.

{{"demo": "pages/components/use-media-query/JavaScriptMedia.js", "defaultCodeOpen": true}}

## Server-side rendering

An implementation of [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) is required on the server, we recommend using [css-mediaquery](https://github.com/ericf/css-mediaquery).
We also encourage the usage of the `useMediaQueryTheme` version of the hook that fetches properties from the theme. This way, you can provide a `ssrMatchMedia` option once for all your React tree.
An implementation of [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) is required on the server.
We recommend using [css-mediaquery](https://github.com/ericf/css-mediaquery) to emulate it.

{{"demo": "pages/components/use-media-query/ServerSide.js"}}

⚠️ Server-side rendering and client-side media queries are fundamentally at odds.
Be aware of the tradeoff. The support can only be partial.

Try relying on client-side CSS media queries first.
For instance, you could use:

- [`<Box display>`](/system/display/#hiding-elements)
- [`<Hidden implementation="css">`](/components/hidden/#css)
- or [`themes.breakpoints.up(x)`](/customization/breakpoints/#css-media-queries)

## Testing

Similar to the server-side case, you need an implementation of [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) in your test environment.

For instance, [jsdom doesn't support it yet](https://github.com/jsdom/jsdom/blob/master/test/web-platform-tests/to-upstream/html/browsers/the-window-object/window-properties-dont-upstream.html). You should polyfill it.
We recommend using [css-mediaquery](https://github.com/ericf/css-mediaquery) to emulate it.

```js
import mediaQuery from 'css-mediaquery';

function createMatchMedia(width) {
return query => ({
matches: mediaQuery.match(query, { width }),
addListener: () => {},
removeListener: () => {},
});
}

describe('MyTests', () => {
beforeAll(() => {
window.matchMedia = createMatchMedia(window.innerWidth);
});
});
```

## Migrating from `withWidth()`

The `withWidth()` higher-order component injects the screen width of the page.
Expand Down Expand Up @@ -84,7 +132,7 @@ function useWidth() {

#### Arguments

1. `query` (*String*): A string representing the media query to handle.
1. `query` (*String* | *Function*): A string representing the media query to handle or a callback function accepting the theme (in the context) that returns a string.
2. `options` (*Object* [optional]):
- `options.defaultMatches` (*Boolean* [optional]):
As `window.matchMedia()` is unavailable on the server,
Expand All @@ -97,7 +145,7 @@ function useWidth() {
- `options.ssrMatchMedia` (*Function* [optional]) You might want to use an heuristic to approximate
the screen of the client browser.
For instance, you could be using the user-agent or the client-hint https://caniuse.com/#search=client%20hint.
You can provide a global ponyfill using [`custom properties`](/customization/globals/#default-props) on the theme. Check the [server-side rendering example](#server-side-rendering).
You can provide a global ponyfill using [`custom props`](/customization/globals/#default-props) on the theme. Check the [server-side rendering example](#server-side-rendering).

#### Returns

Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/styles/advanced/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ If you are using Server-Side Rendering (SSR), you should pass the nonce in the `
```

Then, you must pass this nonce to JSS so it can add it to subsequent `<style>` tags.
The client side gets the nonce from a header. You must include this header regardless of whether or not SSR is used.
The client-side gets the nonce from a header. You must include this header regardless of whether or not SSR is used.

```jsx
<meta property="csp-nonce" content={nonce} />
Expand Down
2 changes: 1 addition & 1 deletion packages/material-ui/src/ButtonBase/ButtonBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) {
{children}
{!disableRipple && !disabled ? (
<NoSsr>
{/* TouchRipple is only needed client side, x2 boost on the server. */}
{/* TouchRipple is only needed client-side, x2 boost on the server. */}
<TouchRipple ref={rippleRef} center={centerRipple} {...TouchRippleProps} />
</NoSsr>
) : null}
Expand Down
2 changes: 1 addition & 1 deletion packages/material-ui/src/Tooltip/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ function Tooltip(props) {

React.useEffect(() => {
// Fallback to this default id when possible.
// Use the random value for client side rendering only.
// Use the random value for client-side rendering only.
// We can't use it server-side.
if (!defaultId.current) {
defaultId.current = `mui-tooltip-${Math.round(Math.random() * 1e5)}`;
Expand Down
5 changes: 4 additions & 1 deletion packages/material-ui/src/useMediaQuery/useMediaQuery.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ export interface Options {
ssrMatchMedia?: (query: string) => MuiMediaQueryList;
}

export default function useMediaQuery(query: string, options?: Options): boolean;
export default function useMediaQuery(
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
query: string | ((theme: any) => string),
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
options?: Options,
): boolean;
63 changes: 36 additions & 27 deletions packages/material-ui/src/useMediaQuery/useMediaQuery.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,53 @@
import React from 'react';
import warning from 'warning';
import { getThemeProps, useTheme } from '@material-ui/styles';

// This variable will be true once the server-side hydration is completed.
let hydrationCompleted = false;

function deepEqual(a, b) {
return a.length === b.length && a.every((item, index) => item === b[index]);
}

function useMediaQuery(queryInput, options = {}) {
const multiple = Array.isArray(queryInput);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove multiple support

let queries = multiple ? queryInput : [queryInput];
queries = queries.map(query => query.replace('@media ', ''));
const theme = useTheme();
const props = getThemeProps({
theme,
name: 'MuiUseMediaQuery',
props: {},
});

warning(
typeof queryInput !== 'function' || theme !== null,
[
'Material-UI: the `query` argument provided is invalid.',
'You are providing a function without a theme in the context.',
'One of the parent elements needs to use a ThemeProvider.',
].join('\n'),
);

let query = typeof queryInput === 'function' ? queryInput(theme) : queryInput;
query = query.replace('@media ', '');
merceyz marked this conversation as resolved.
Show resolved Hide resolved

// Wait for JSDOM to support the match media feature.
// Wait for jsdom to support the match media feature.
// All the browsers Material-UI support have this built-in.
// This defensive check is here for simplicity.
// Most of the time, the match media logic isn't central to people tests.
const supportMatchMedia =
typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined';

const { defaultMatches = false, noSsr = false, ssrMatchMedia = null } = options;
const { defaultMatches = false, noSsr = false, ssrMatchMedia = null } = {
...props,
...options,
};

const [matches, setMatches] = React.useState(() => {
const [match, setMatch] = React.useState(() => {
if ((hydrationCompleted || noSsr) && supportMatchMedia) {
return queries.map(query => window.matchMedia(query).matches);
return window.matchMedia(query).matches;
}
if (ssrMatchMedia) {
return queries.map(query => ssrMatchMedia(query).matches);
return ssrMatchMedia(query).matches;
}

// Once the component is mounted, we rely on the
// event listeners to return the correct matches value.
return queries.map(() => defaultMatches);
return defaultMatches;
});

React.useEffect(() => {
Expand All @@ -41,27 +57,20 @@ function useMediaQuery(queryInput, options = {}) {
return undefined;
}

const queryLists = queries.map(query => window.matchMedia(query));
setMatches(prev => {
const next = queryLists.map(queryList => queryList.matches);
return deepEqual(prev, next) ? prev : next;
});
const queryList = window.matchMedia(query);
setMatch(queryList.matches);

function handleMatchesChange() {
setMatches(queryLists.map(queryList => queryList.matches));
setMatch(queryList.matches);
}

queryLists.forEach(queryList => {
queryList.addListener(handleMatchesChange);
});
queryList.addListener(handleMatchesChange);
return () => {
queryLists.forEach(queryList => {
queryList.removeListener(handleMatchesChange);
});
queryList.removeListener(handleMatchesChange);
};
}, queries); // eslint-disable-line react-hooks/exhaustive-deps
}, [query]); // eslint-disable-line react-hooks/exhaustive-deps
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved

return multiple ? matches : matches[0];
return match;
}

export function testReset() {
Expand Down
Loading