-
Notifications
You must be signed in to change notification settings - Fork 0
⚡ Bolt: Optimize ScheduleContainer Rendering #43
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
base: main
Are you sure you want to change the base?
Changes from all commits
7d24a5a
eb4da3a
d2f7618
f6361b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import ScheduleContainer from "../../../components/schedule/ScheduleContainer"; | ||
| import { DailySchedule } from "../../../hooks/useSchedule"; | ||
| import { useMediaQuery } from "../../../hooks/useMediaQuery"; | ||
|
|
||
| // Mock the hook | ||
| jest.mock("../../../hooks/useMediaQuery"); | ||
|
|
||
| // Mock child components to verify rendering | ||
| jest.mock("../../../components/schedule/ScheduleGrid", () => { | ||
| const MockScheduleGrid = () => <div data-testid="schedule-grid">Grid</div>; | ||
| MockScheduleGrid.displayName = "MockScheduleGrid"; | ||
| return MockScheduleGrid; | ||
| }); | ||
|
|
||
| jest.mock("../../../components/schedule/ScheduleMobile", () => { | ||
| const MockScheduleMobile = () => <div data-testid="schedule-mobile">Mobile</div>; | ||
| MockScheduleMobile.displayName = "MockScheduleMobile"; | ||
| return MockScheduleMobile; | ||
| }); | ||
|
|
||
| // Mock context | ||
| jest.mock("../../../context/ScheduleContext", () => ({ | ||
| useScheduleContext: () => ({ | ||
| savedSessionIds: [], | ||
| isSaved: () => false, | ||
| toggleSession: jest.fn(), | ||
| }), | ||
| })); | ||
|
|
||
| const mockSchedule: DailySchedule[] = []; | ||
|
|
||
| describe("ScheduleContainer Optimization", () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("renders both components initially (SSR/Hydration)", () => { | ||
| (useMediaQuery as jest.Mock).mockReturnValue(null); | ||
| render(<ScheduleContainer initialSchedule={mockSchedule} year="2024" />); | ||
|
|
||
| // Both should be present to support hydration matching and CSS hiding | ||
| expect(screen.getByTestId("schedule-grid")).toBeInTheDocument(); | ||
| expect(screen.getByTestId("schedule-mobile")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders only ScheduleGrid on Desktop", () => { | ||
| (useMediaQuery as jest.Mock).mockReturnValue(false); | ||
| render(<ScheduleContainer initialSchedule={mockSchedule} year="2024" />); | ||
|
|
||
| expect(screen.getByTestId("schedule-grid")).toBeInTheDocument(); | ||
| expect(screen.queryByTestId("schedule-mobile")).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders only ScheduleMobile on Mobile", () => { | ||
| (useMediaQuery as jest.Mock).mockReturnValue(true); | ||
| render(<ScheduleContainer initialSchedule={mockSchedule} year="2024" />); | ||
|
|
||
| expect(screen.queryByTestId("schedule-grid")).not.toBeInTheDocument(); | ||
| expect(screen.getByTestId("schedule-mobile")).toBeInTheDocument(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ScheduleGrid from "./ScheduleGrid"; | |
| import ScheduleMobile from "./ScheduleMobile"; | ||
| import { useScheduleContext } from "@/context/ScheduleContext"; | ||
| import styles from "./schedule.module.scss"; | ||
| import { useMediaQuery } from "@/hooks/useMediaQuery"; | ||
|
|
||
| interface ScheduleContainerProps { | ||
| initialSchedule: DailySchedule[]; | ||
|
|
@@ -15,6 +16,8 @@ interface ScheduleContainerProps { | |
| export default function ScheduleContainer({ initialSchedule, year }: ScheduleContainerProps) { | ||
| const { savedSessionIds } = useScheduleContext(); | ||
| const [showSavedOnly, setShowSavedOnly] = useState(false); | ||
| const isMobile = useMediaQuery("(max-width: 991px)"); | ||
|
|
||
| const filteredSchedule = useMemo(() => { | ||
| if (!showSavedOnly) { | ||
| return initialSchedule; | ||
|
|
@@ -46,12 +49,24 @@ export default function ScheduleContainer({ initialSchedule, year }: ScheduleCon | |
| </button> | ||
| </div> | ||
|
|
||
| <div className="d-none d-lg-block"> | ||
| <ScheduleGrid schedule={filteredSchedule} year={year} /> | ||
| </div> | ||
| <div className="d-lg-none"> | ||
| <ScheduleMobile schedule={filteredSchedule} year={year} /> | ||
| </div> | ||
| {/* | ||
| Optimization: Only render the visible component to reduce DOM nodes. | ||
| Render both (hidden via CSS) only during SSR/initial hydration (isMobile === null). | ||
| */} | ||
|
|
||
| {/* Desktop View */} | ||
| {(isMobile === null || isMobile === false) && ( | ||
| <div className="d-none d-lg-block"> | ||
| <ScheduleGrid schedule={filteredSchedule} year={year} /> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* Mobile View */} | ||
| {(isMobile === null || isMobile === true) && ( | ||
| <div className="d-lg-none"> | ||
| <ScheduleMobile schedule={filteredSchedule} year={year} /> | ||
| </div> | ||
| )} | ||
|
Comment on lines
+58
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The conditional rendering logic is correct, but it can be simplified for better readability. The expressions |
||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| "use client"; | ||
|
|
||
| import { useState, useEffect } from "react"; | ||
|
|
||
| /** | ||
| * Custom hook to detect if a media query matches. | ||
| * Returns null during SSR/hydration to avoid mismatches, | ||
| * and then updates to true/false on the client. | ||
| */ | ||
| export function useMediaQuery(query: string): boolean | null { | ||
| const [matches, setMatches] = useState<boolean | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| const media = window.matchMedia(query); | ||
| if (media.matches !== matches) { | ||
| setMatches(media.matches); | ||
| } | ||
| const listener = (e: MediaQueryListEvent) => setMatches(e.matches); | ||
|
|
||
| // Support for older browsers and modern ones | ||
| if (media.addEventListener) { | ||
| media.addEventListener("change", listener); | ||
| } else { | ||
| media.addListener(listener); | ||
| } | ||
| return () => { | ||
| if (media.removeEventListener) { | ||
| media.removeEventListener("change", listener); | ||
| } else { | ||
| media.removeListener(listener); | ||
| } | ||
| }; | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [query]); | ||
|
Comment on lines
+13
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The useEffect(() => {
const media = window.matchMedia(query);
// Set the initial value on the client.
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
// Support for older browsers and modern ones
if (media.addEventListener) {
media.addEventListener("change", listener);
} else {
media.addListener(listener);
}
return () => {
if (media.removeEventListener) {
media.removeEventListener("change", listener);
} else {
media.removeListener(listener);
}
};
}, [query]); |
||
|
|
||
| return matches; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The media query string
(max-width: 991px)is hardcoded. This is considered a "magic string" and can make maintenance harder. If this breakpoint is used elsewhere or needs to be updated, it would have to be changed in multiple places. It's a best practice to define such values as constants in a shared theme or constants file and reference it here, for example:const isMobile = useMediaQuery(breakpoints.lg);.