Skip to content

Commit

Permalink
[useMediaQuery] Defensive logic against matchMedia not available
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed Jun 12, 2019
1 parent cddc4cf commit 2b0f8f9
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 107 deletions.
2 changes: 1 addition & 1 deletion docs/src/modules/components/AppTableOfContents.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function useThrottledOnScroll(callback, delay) {

React.useEffect(() => {
if (throttledCallback === noop) {
return () => {};
return undefined;
}

window.addEventListener('scroll', throttledCallback);
Expand Down
12 changes: 11 additions & 1 deletion packages/material-ui/src/useMediaQuery/useMediaQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ function useMediaQuery(queryInput, options = {}) {
let queries = multiple ? queryInput : [queryInput];
queries = queries.map(query => query.replace('@media ', ''));

// 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.matchMedia !== 'undefined';

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

const [matches, setMatches] = React.useState(() => {
if (hydrationCompleted || noSsr) {
if ((hydrationCompleted || noSsr) && supportMatchMedia) {
return queries.map(query => window.matchMedia(query).matches);
}
if (ssrMatchMedia) {
Expand All @@ -30,6 +36,10 @@ function useMediaQuery(queryInput, options = {}) {
React.useEffect(() => {
hydrationCompleted = true;

if (!supportMatchMedia) {
return undefined;
}

const queryLists = queries.map(query => window.matchMedia(query));
setMatches(prev => {
const next = queryLists.map(queryList => queryList.matches);
Expand Down
227 changes: 122 additions & 105 deletions packages/material-ui/src/useMediaQuery/useMediaQuery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,33 +48,117 @@ describe('useMediaQuery', () => {
mount = createMount({ strict: true });
});

beforeEach(() => {
testReset();
values = spy();
matchMediaInstances = [];
window.matchMedia = createMatchMedia(1200, matchMediaInstances);
});

after(() => {
mount.cleanUp();
});

describe('option: defaultMatches', () => {
it('should be false by default', () => {
describe('without feature', () => {
it('should work without window.matchMedia available', () => {
assert.strictEqual(typeof window.matchMedia, 'undefined');
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)');
React.useEffect(() => values(matches));
const matches = useMediaQuery('(min-width:100px)');
return <span ref={ref}>{`${matches}`}</span>;
};

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 1);
});
});

describe('with feature', () => {
beforeEach(() => {
testReset();
values = spy();
matchMediaInstances = [];
window.matchMedia = createMatchMedia(1200, matchMediaInstances);
});

describe('option: defaultMatches', () => {
it('should be false by default', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)');
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 1);
});

it('should take the option into account', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
defaultMatches: true,
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 2);
});
});

describe('option: noSsr', () => {
it('should render once if the default value match the expectation', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
defaultMatches: false,
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 1);
});

it('should render twice if the default value does not match the expectation', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
defaultMatches: true,
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 2);
});

it('should take the option into account', () => {
it('should render once if the default value does not match the expectation', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
defaultMatches: true,
noSsr: true,
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 1);
});
});

it('should try to reconcile only the first time', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
Expand All @@ -88,125 +172,58 @@ describe('useMediaQuery', () => {
mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 2);
});
});

describe('option: noSsr', () => {
it('should render once if the default value match the expectation', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
defaultMatches: false,
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};
ReactDOM.unmountComponentAtNode(mount.attachTo);

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 1);
assert.strictEqual(values.callCount, 3);
});

it('should render twice if the default value does not match the expectation', () => {
it('should be able to change the query dynamically', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
const Test = props => {
const matches = useMediaQuery(props.query, {
defaultMatches: true,
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};
Test.propTypes = {
query: PropTypes.string.isRequired,
};

mount(<Test />);
const wrapper = mount(<Test query="(min-width:2000px)" />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 2);
wrapper.setProps({ query: '(min-width:100px)' });
assert.strictEqual(text(), 'true');
assert.strictEqual(values.callCount, 4);
});

it('should render once if the default value does not match the expectation', () => {
it('should observe the media query', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
defaultMatches: true,
noSsr: true,
});
const Test = props => {
const matches = useMediaQuery(props.query);
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};
Test.propTypes = {
query: PropTypes.string.isRequired,
};

mount(<Test />);
assert.strictEqual(text(), 'false');
mount(<Test query="(min-width:2000px)" />);
assert.strictEqual(values.callCount, 1);
});
});

it('should try to reconcile only the first time', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
defaultMatches: true,
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 2);

ReactDOM.unmountComponentAtNode(mount.attachTo);

mount(<Test />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 3);
});
assert.strictEqual(text(), 'false');

it('should be able to change the query dynamically', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = props => {
const matches = useMediaQuery(props.query, {
defaultMatches: true,
act(() => {
matchMediaInstances[0].instance.matches = true;
matchMediaInstances[0].listeners[0]();
});
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};
Test.propTypes = {
query: PropTypes.string.isRequired,
};

const wrapper = mount(<Test query="(min-width:2000px)" />);
assert.strictEqual(text(), 'false');
assert.strictEqual(values.callCount, 2);
wrapper.setProps({ query: '(min-width:100px)' });
assert.strictEqual(text(), 'true');
assert.strictEqual(values.callCount, 4);
});

it('should observe the media query', () => {
const ref = React.createRef();
const text = () => ref.current.textContent;
const Test = props => {
const matches = useMediaQuery(props.query);
React.useEffect(() => values(matches));
return <span ref={ref}>{`${matches}`}</span>;
};
Test.propTypes = {
query: PropTypes.string.isRequired,
};

mount(<Test query="(min-width:2000px)" />);
assert.strictEqual(values.callCount, 1);
assert.strictEqual(text(), 'false');

act(() => {
matchMediaInstances[0].instance.matches = true;
matchMediaInstances[0].listeners[0]();
assert.strictEqual(text(), 'true');
assert.strictEqual(values.callCount, 2);
});
assert.strictEqual(text(), 'true');
assert.strictEqual(values.callCount, 2);
});
});

0 comments on commit 2b0f8f9

Please sign in to comment.