Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/components/codeblock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const styles = {
},
};

export default function CodeBlock({ code, bordered = false }: CodeBlockProps) {
export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const [highlighted, setHighlighted] = useState(false);
const theme = useTheme();
Expand Down Expand Up @@ -111,4 +111,4 @@ export default function CodeBlock({ code, bordered = false }: CodeBlockProps) {
</Box>
</Box>
);
}
});
12 changes: 6 additions & 6 deletions frontend/src/pages/leaderboard/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Typography,
} from "@mui/material";
import Grid from "@mui/material/Grid";
import { useCallback, useEffect, useState } from "react";
import { memo, useCallback, useEffect, useState } from "react";
import { fetchLeaderBoard, searchUsers } from "../../api/api";
import { fetcherApiCallback } from "../../lib/hooks/useApi";
import { isExpired, toDateUtc } from "../../lib/date/utils";
Expand All @@ -27,7 +27,7 @@ import LeaderboardSubmit from "./components/LeaderboardSubmit";
import UserTrendChart from "./components/UserTrendChart";
import {
SubmissionSidebarProvider,
useSubmissionSidebar,
useSubmissionSidebarState,
} from "./components/SubmissionSidebarContext";
import SubmissionCodeSidebar from "./components/SubmissionCodeSidebar";

Expand Down Expand Up @@ -68,8 +68,8 @@ function TabPanel(props: {
);
}

// Inner component that uses the sidebar context
function LeaderboardContent() {
// Inner component
const LeaderboardContent = memo(function LeaderboardContent() {
const { id } = useParams<{ id: string }>();

const { data, loading, error, errorStatus, call } =
Expand Down Expand Up @@ -338,7 +338,7 @@ function LeaderboardContent() {
</Box>
</ConstrainedContainer>
);
}
});

// Main wrapper component with sidebar provider and flex layout
export default function Leaderboard() {
Expand All @@ -360,7 +360,7 @@ function LeaderboardWithSidebar() {
isLoadingCodes,
navigate,
close,
} = useSubmissionSidebar();
} = useSubmissionSidebarState();

const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/leaderboard/components/RankingLists.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
NavigationItem,
SelectedSubmission,
} from "./submissionTypes";
import { useSubmissionSidebar } from "./SubmissionSidebarContext";
import { useSubmissionSidebarActions } from "./SubmissionSidebarContext";
import { isExpired } from "../../../lib/date/utils.ts";
import { useAuthStore } from "../../../lib/store/authStore.ts";

Expand Down Expand Up @@ -98,7 +98,7 @@ export default function RankingsList({
const [colorHash] = useState<string>(
Math.random().toString(36).slice(2, 8),
);
const { openSubmission } = useSubmissionSidebar();
const { openSubmission } = useSubmissionSidebarActions();

const toggleExpanded = (field: string) => {
setExpanded((prev) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import { createContext, useState, useCallback, useContext } from "react";
import { createContext, useState, useCallback, useContext, useRef } from "react";
import type { NavigationItem, SelectedSubmission } from "./submissionTypes";
import { fetchCodes } from "../../../api/api";

interface SubmissionSidebarContextType {
selectedSubmission: SelectedSubmission | null;
navigationItems: NavigationItem[];
navigationIndex: number;
codes: Map<number, string>;
isOpen: boolean;
isLoadingCodes: boolean;
// Actions context — stable references, rarely changes
interface SubmissionSidebarActionsType {
openSubmission: (
submission: SelectedSubmission,
navItems: NavigationItem[],
navIndex: number,
leaderboardId: string | number
) => void;
}

// State context — changes on every navigation
interface SubmissionSidebarStateType {
selectedSubmission: SelectedSubmission | null;
navigationItems: NavigationItem[];
navigationIndex: number;
codes: Map<number, string>;
isOpen: boolean;
isLoadingCodes: boolean;
navigate: (newIndex: number, item: NavigationItem) => void;
close: () => void;
}

const SubmissionSidebarContext =
createContext<SubmissionSidebarContextType | null>(null);
const SubmissionSidebarActionsContext =
createContext<SubmissionSidebarActionsType | null>(null);

const SubmissionSidebarStateContext =
createContext<SubmissionSidebarStateType | null>(null);

export function SubmissionSidebarProvider({
children,
Expand All @@ -33,6 +41,8 @@ export function SubmissionSidebarProvider({
const [navigationIndex, setNavigationIndex] = useState(0);
const [codes, setCodes] = useState<Map<number, string>>(new Map());
const [isLoadingCodes, setIsLoadingCodes] = useState(false);
const codesRef = useRef(codes);
codesRef.current = codes;

const openSubmission = useCallback(
(
Expand All @@ -49,7 +59,7 @@ export function SubmissionSidebarProvider({
// Find submission IDs that aren't already cached
const idsToFetch = navItems
.map((item) => item.submissionId)
.filter((id) => !codes.has(id));
.filter((id) => !codesRef.current.has(id));

if (idsToFetch.length > 0) {
setIsLoadingCodes(true);
Expand All @@ -71,7 +81,7 @@ export function SubmissionSidebarProvider({
});
}
},
[codes]
[]
);

const navigate = useCallback((newIndex: number, item: NavigationItem) => {
Expand All @@ -96,30 +106,44 @@ export function SubmissionSidebarProvider({
}, []);

return (
<SubmissionSidebarContext.Provider
value={{
selectedSubmission,
navigationItems,
navigationIndex,
codes,
isOpen: !!selectedSubmission,
isLoadingCodes,
openSubmission,
navigate,
close,
}}
>
{children}
</SubmissionSidebarContext.Provider>
<SubmissionSidebarActionsContext.Provider value={{ openSubmission }}>
Comment thread
yangw-dev marked this conversation as resolved.
<SubmissionSidebarStateContext.Provider
value={{
selectedSubmission,
navigationItems,
navigationIndex,
codes,
isOpen: !!selectedSubmission,
isLoadingCodes,
navigate,
close,
}}
>
{children}
</SubmissionSidebarStateContext.Provider>
</SubmissionSidebarActionsContext.Provider>
);
}

// For components that only need to open the sidebar (RankingsList, UserTrendChart)
// eslint-disable-next-line react-refresh/only-export-components
export function useSubmissionSidebarActions(): SubmissionSidebarActionsType {
const context = useContext(SubmissionSidebarActionsContext);
if (!context) {
throw new Error(
"useSubmissionSidebarActions must be used within SubmissionSidebarProvider"
);
}
return context;
}

// For the sidebar component that needs the full state
// eslint-disable-next-line react-refresh/only-export-components
export function useSubmissionSidebar(): SubmissionSidebarContextType {
const context = useContext(SubmissionSidebarContext);
export function useSubmissionSidebarState(): SubmissionSidebarStateType {
const context = useContext(SubmissionSidebarStateContext);
if (!context) {
throw new Error(
"useSubmissionSidebar must be used within SubmissionSidebarProvider"
"useSubmissionSidebarState must be used within SubmissionSidebarProvider"
);
}
return context;
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/leaderboard/components/UserTrendChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import type {
NavigationItem,
SelectedSubmission,
} from "./submissionTypes";
import { useSubmissionSidebar } from "./SubmissionSidebarContext";
import { useSubmissionSidebarActions } from "./SubmissionSidebarContext";

// Display name prefix for custom (KernelAgent) entries
const CUSTOM_ENTRY_PREFIX = "KernelAgent";
Expand Down Expand Up @@ -168,7 +168,7 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
const isCodeViewingAllowed = isContestClosed;

// Use sidebar context instead of local state
const { openSubmission } = useSubmissionSidebar();
const { openSubmission } = useSubmissionSidebarActions();

const [data, setData] = useState<UserTrendResponse | null>(null);
const [customData, setCustomData] = useState<CustomTrendResponse | null>(null);
Expand Down
Loading