Skip to content

Commit

Permalink
perf(responsive): reduce number of window breakpoint listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
abelflopes committed Nov 22, 2024
1 parent 63cfefa commit c5ce48a
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 24 deletions.
29 changes: 5 additions & 24 deletions packages/components/responsive/src/hooks/breakpoints.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { type ResponsiveTarget, type EnabledBreakpointsMapping, type Breakpoint } from "../types";
import { breakpointKeys, breakpoints } from "../constants";
import { breakpoints } from "../constants";
import { eachBreakpoint } from "../utils";
import { breakpointEvents } from "../utils/breakpoint-events";

const baseBreakpointsData: EnabledBreakpointsMapping = {
xs: false,
Expand Down Expand Up @@ -80,35 +81,15 @@ export const useBreakpoints = (
useEffect(() => {
if (!computedActive || computedTarget !== "viewport") return;

const removeListeners = breakpointKeys.map((bpKey) => {
const data = window.matchMedia(`(min-width: ${breakpoints[bpKey]}px)`);

const unsubscribe = breakpointEvents.subscribe(({ breakpoint, matches }) => {
setBreakpointsData((v) => ({
...baseBreakpointsData,
...v,
[bpKey]: data.matches,
[breakpoint]: matches,
}));

const listener = (e: MediaQueryListEventMap["change"]): void => {
setBreakpointsData((v) => ({
...baseBreakpointsData,
...v,
[bpKey]: e.matches,
}));
};

data.addEventListener("change", listener);

return (): void => {
data.removeEventListener("change", listener);
};
});

return () => {
removeListeners.forEach((removeListener) => {
removeListener();
});
};
return unsubscribe;
}, [computedActive, computedTarget]);

return useMemo(
Expand Down
70 changes: 70 additions & 0 deletions packages/components/responsive/src/utils/breakpoint-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { type Breakpoint } from "../types";
import { breakpointKeys, breakpoints } from "../constants";

interface BreakpointEventData {
breakpoint: Breakpoint;
matches: boolean;
}

type BreakpointEventsUnsubscribe = () => void;

type BreakpointEventsHandler = (data: BreakpointEventData) => void;

class BreakpointEvents {
private globalListeners: Array<() => void> = [];

private handlers: BreakpointEventsHandler[] = [];

private listenTimeout: ReturnType<typeof requestAnimationFrame> | undefined = undefined;

public destroy = (): void => {
if (this.listenTimeout) cancelAnimationFrame(this.listenTimeout);
this.listenTimeout = undefined;
this.globalListeners.forEach((cb) => {
cb();
});
this.globalListeners = [];
};

public subscribe = (handler: BreakpointEventsHandler): BreakpointEventsUnsubscribe => {
this.destroy();
this.handlers.push(handler);

this.listenTimeout = requestAnimationFrame(this.listen);

return () => {
this.handlers = this.handlers.filter((i) => i !== handler);
};
};

private readonly listen = (): void => {
this.globalListeners = breakpointKeys.map((bpKey) => {
// TODO: reduce number of listeners
const data = window.matchMedia(`(min-width: ${breakpoints[bpKey]}px)`);

this.handlers.forEach((handler) => {
handler({
breakpoint: bpKey,
matches: data.matches,
});
});

const listener = (e: MediaQueryListEventMap["change"]): void => {
this.handlers.forEach((handler) => {
handler({
breakpoint: bpKey,
matches: e.matches,
});
});
};

data.addEventListener("change", listener);

return (): void => {
data.removeEventListener("change", listener);
};
});
};
}

export const breakpointEvents = new BreakpointEvents();

0 comments on commit c5ce48a

Please sign in to comment.