Skip to content

Commit

Permalink
Executing useBreakpoints isormophically no longer triggers a Hydrat…
Browse files Browse the repository at this point in the history
…ion mismatch error or rendering bugs.
  • Loading branch information
jesstelford committed Oct 5, 2023
1 parent 1c60bf0 commit 52e3b38
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-walls-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris': patch
---

Executing `useBreakpoints` isormophically no longer triggers a Hydration mismatch error or rendering bugs.
23 changes: 20 additions & 3 deletions polaris-react/src/utilities/breakpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,15 @@ type BreakpointsMatches = {

const breakpointsQueryEntries = getBreakpointsQueryEntries(breakpoints);

function getMatches(defaults?: UseBreakpointsOptions['defaults']) {
if (!isServer) {
function getMatches(
defaults?: UseBreakpointsOptions['defaults'],
/**
* Used to force defaults on initial client side render so they match SSR
* values and hence avoid a Hydration error.
*/
forceDefaults?: boolean,
) {
if (!isServer && !forceDefaults) {
return Object.fromEntries(
breakpointsQueryEntries.map(([directionAlias, query]) => [
directionAlias,
Expand Down Expand Up @@ -115,7 +122,13 @@ export interface UseBreakpointsOptions {
* breakpoints //=> All values will be `true` during SSR
*/
export function useBreakpoints(options?: UseBreakpointsOptions) {
const [breakpoints, setBreakpoints] = useState(getMatches(options?.defaults));
// On SSR, and initial CSR, we force usage of the defaults to avoid a
// hydration mismatch error.
// Later, in the effect, we will call this again on the client side without
// any defaults to trigger a more accurate client side evaluation.
const [breakpoints, setBreakpoints] = useState(
getMatches(options?.defaults, true),
);

useIsomorphicLayoutEffect(() => {
const mediaQueryLists = breakpointsQueryEntries.map(([_, query]) =>
Expand All @@ -132,6 +145,10 @@ export function useBreakpoints(options?: UseBreakpointsOptions) {
}
});

// Trigger the breakpoint recalculation at least once client-side to ensure
// we don't have stale default values from SSR.
handler();

return () => {
mediaQueryLists.forEach((mql) => {
if (mql.removeListener) {
Expand Down
127 changes: 88 additions & 39 deletions polaris-react/src/utilities/tests/use-breakpoints.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,89 +18,138 @@ describe('useBreakpoints', () => {
matchMedia.restore();
});

it('breakpoints-xs', () => {
it('initial render uses defaults', () => {
setMediaWidth('breakpoints-xs');
let breakpoints;
let renders = 0;
mount(<MockComponent />);

function MockComponent() {
const breakpoints = useBreakpoints();

expect(breakpoints).toMatchObject({
xsDown: true,
xsOnly: true,
xsUp: true,
renders++;
breakpoints = useBreakpoints({
defaults: {
mdDown: true,
mdOnly: true,
mdUp: true,
},
});

expect(breakpoints).toMatchObject(
renders === 1
? {
xsDown: false,
xsOnly: false,
xsUp: false,
mdDown: true,
mdOnly: true,
mdUp: true,
}
: {
xsDown: true,
xsOnly: true,
xsUp: true,
mdDown: false,
mdOnly: false,
mdUp: false,
},
);

return null;
}

expect(breakpoints).toMatchObject({
xsDown: true,
xsOnly: true,
xsUp: true,
mdDown: false,
mdOnly: false,
mdUp: false,
});
});

it('breakpoints-sm', () => {
setMediaWidth('breakpoints-sm');
it('breakpoints-xs', () => {
setMediaWidth('breakpoints-xs');
let breakpoints;
mount(<MockComponent />);

function MockComponent() {
const breakpoints = useBreakpoints();
breakpoints = useBreakpoints();
return null;
}

expect(breakpoints).toMatchObject({
smDown: true,
smOnly: true,
smUp: true,
});
expect(breakpoints).toMatchObject({
xsDown: true,
xsOnly: true,
xsUp: true,
});
});

it('breakpoints-sm', () => {
setMediaWidth('breakpoints-sm');
let breakpoints;
mount(<MockComponent />);

function MockComponent() {
breakpoints = useBreakpoints();
return null;
}

expect(breakpoints).toMatchObject({
smDown: true,
smOnly: true,
smUp: true,
});
});

it('breakpoints-md', () => {
setMediaWidth('breakpoints-md');
let breakpoints;
mount(<MockComponent />);

function MockComponent() {
const breakpoints = useBreakpoints();

expect(breakpoints).toMatchObject({
mdDown: true,
mdOnly: true,
mdUp: true,
});

breakpoints = useBreakpoints();
return null;
}

expect(breakpoints).toMatchObject({
mdDown: true,
mdOnly: true,
mdUp: true,
});
});

it('breakpoints-lg', () => {
setMediaWidth('breakpoints-lg');
let breakpoints;
mount(<MockComponent />);

function MockComponent() {
const breakpoints = useBreakpoints();

expect(breakpoints).toMatchObject({
lgDown: true,
lgOnly: true,
lgUp: true,
});

breakpoints = useBreakpoints();
return null;
}

expect(breakpoints).toMatchObject({
lgDown: true,
lgOnly: true,
lgUp: true,
});
});

it('breakpoints-xl', () => {
setMediaWidth('breakpoints-xl');
let breakpoints;
mount(<MockComponent />);

function MockComponent() {
const breakpoints = useBreakpoints();

expect(breakpoints).toMatchObject({
xlDown: true,
xlOnly: true,
xlUp: true,
});

breakpoints = useBreakpoints();
return null;
}

expect(breakpoints).toMatchObject({
xlDown: true,
xlOnly: true,
xlUp: true,
});
});
});

Expand Down

0 comments on commit 52e3b38

Please sign in to comment.