Write useMediaQuery(query) and export it from useMediaQuery.js. It returns true while a CSS media query matches, like "(min-width: 768px)" for the tea shop's two-column layout, and false while it doesn't.
window.matchMedia(query) returns a MediaQueryList. Its matches says whether the query matches now, and it fires a change event when that changes. That makes it an external store, so read it with useSyncExternalStore: subscribe with addEventListener("change", …), and remove that listener in the unsubscribe function.
The answer is right on the very first render in the browser, with no flash of the wrong layout. When query changes, answer the new query and stop listening to the old one. Stop listening when the component goes away.
On the server there's no matchMedia, so the server snapshot is false. When React hydrates HTML that came from the server, its first render uses false too, then it switches to the real answer.
function Menu() {
const wide = useMediaQuery("(min-width: 768px)");
return <p>{wide ? "Two columns" : "One column"}</p>;
}
The Preview shows Demo.jsx, which uses your hook. Make the Preview wider and narrower to see it change.
mockMatchMedia(false);
const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
expect(result.current).toBe(false);
const media = mockMatchMedia((query) => query === "(min-width: 768px)");
const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
expect(result.current).toBe(true);
expect(media.queries()).toContain("(min-width: 768px)");
mockMatchMedia(true);
const seen = [];
function Menu() {
const wide = useMediaQuery("(min-width: 768px)");
seen.push(wide);
return <p>{wide ? "Two columns" : "One column"}</p>;
}
render(<Menu />);
expect(seen[0]).toBe(true);
const media = mockMatchMedia(false);
const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
await media.set(true);
expect(result.current).toBe(true);
await media.set(false);
expect(result.current).toBe(false);
const media = mockMatchMedia(false);
function Menu() {
const wide = useMediaQuery("(min-width: 768px)");
return <p>{wide ? "Two columns" : "One column"}</p>;
}
render(<Menu />);
expect(screen.getByText("One column")).toBeInTheDocument();
await media.set(true);
expect(screen.getByText("Two columns")).toBeInTheDocument();
const media = mockMatchMedia((query) => query === "(min-width: 1024px)");
const { result, rerender } = renderHook(({ query }) => useMediaQuery(query), { initialProps: { query: "(min-width: 768px)" } });
expect(result.current).toBe(false);
rerender({ query: "(min-width: 1024px)" });
expect(result.current).toBe(true);
await media.set(false);
expect(result.current).toBe(false);
mockMatchMedia(false);
const make = window.matchMedia;
const active = [];
window.matchMedia = (query) => {
const list = make(query);
const add = list.addEventListener;
const remove = list.removeEventListener;
list.addEventListener = (type, listener) => {
if (type === "change") active.push({ list, listener });
add(type, listener);
};
list.removeEventListener = (type, listener) => {
const at = active.findIndex((entry) => entry.list === list && entry.listener === listener);
if (type === "change" && at !== -1) active.splice(at, 1);
remove(type, listener);
};
return list;
};
const listening = (query) => active.filter((entry) => entry.list.media === query).length;
const { rerender, unmount } = renderHook(({ query }) => useMediaQuery(query), { initialProps: { query: "(min-width: 768px)" } });
expect(listening("(min-width: 768px)")).toBe(1);
rerender({ query: "(min-width: 1024px)" });
expect(listening("(min-width: 768px)")).toBe(0);
expect(listening("(min-width: 1024px)")).toBe(1);
unmount();
expect(listening("(min-width: 1024px)")).toBe(0);
mockMatchMedia(true);
const seen = [];
function Menu() {
const wide = useMediaQuery("(min-width: 768px)");
seen.push(wide);
return <p>{wide ? "Two columns" : "One column"}</p>;
}
const container = document.createElement("div");
container.innerHTML = "<p>One column</p>";
document.body.append(container);
render(<Menu />, { container, hydrate: true });
expect(seen[0]).toBe(false);
expect(await screen.findByText("Two columns")).toBeInTheDocument();
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with useMediaQuery. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.