@@ -105,9 +101,7 @@ export function GsocStats() {
);
}
- // A member picked in the other year group is not in this list; falling back
- // to "all" beats rendering an empty breakdown that looks like real data.
- const activeMember = data.members.some(m => m.github === selectedMember) ? selectedMember : 'all';
+ const activeMember = selectedMember;
const filteredGsocPRs = activeMember === 'all'
? data.members.flatMap(m => m.gsocPRs)
: data.members.find(m => m.github === activeMember)?.gsocPRs || [];
diff --git a/components/pr-stats.tsx b/components/pr-stats.tsx
index 7eb1e1e..818b79a 100644
--- a/components/pr-stats.tsx
+++ b/components/pr-stats.tsx
@@ -79,13 +79,7 @@ export function PRStats() {
return () => clearInterval(refreshInterval);
}, [cohort.id]);
- // While the next year group is still loading, `stats` still holds the last
- // one. Derived rather than cleared in the effect: clearing would mean a
- // setState during render-effect, and the question "is this data for the year
- // the URL asks for?" is answerable from what we already have.
- const stale = stats !== null && stats.year !== cohort.id;
-
- if (loading || error || !stats || stale) {
+ if (loading || error || !stats) {
return (
{/* The switcher stays on screen while loading. Counting the second
diff --git a/components/quality-prs-list.tsx b/components/quality-prs-list.tsx
index be88e1f..8b45d2e 100644
--- a/components/quality-prs-list.tsx
+++ b/components/quality-prs-list.tsx
@@ -64,12 +64,7 @@ export function QualityPRsList() {
fetchPRs();
}, [cohort.id]);
- // Derived, not cleared in the effect: while the next year group loads, the
- // previous one's pull requests are still in state and must not render under
- // the new heading.
- const stale = data !== null && data.year !== cohort.id;
-
- if (loading || error || !data || stale) {
+ if (loading || error || !data) {
return (
@@ -87,9 +82,7 @@ export function QualityPRsList() {
// Get unique authors for filter
const authors = Array.from(new Set(data.prs.map(pr => pr.author.github)));
- // A filter left over from the other year group names somebody who is not in
- // this list, which would silently render zero results. Fall back to "all".
- const activeAuthor = authors.includes(filterAuthor) ? filterAuthor : 'all';
+ const activeAuthor = filterAuthor;
// Filter and sort PRs
let filteredPRs = activeAuthor === 'all'
diff --git a/components/ui/cohort-scope.tsx b/components/ui/cohort-scope.tsx
new file mode 100644
index 0000000..5a9cb47
--- /dev/null
+++ b/components/ui/cohort-scope.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { Fragment, type ReactNode } from "react";
+import { useSearchParams } from "next/navigation";
+import { cohortFromParam } from "@/lib/cohorts";
+
+/**
+ * Remounts its subtree when the selected year group changes.
+ *
+ * Switching year replaces every piece of state a page holds: the fetched
+ * numbers, whether a fetch is in flight, any error from the last one, and the
+ * author or member filters, which list people who are not in the other cohort.
+ * Resetting those one by one inside an effect is how this broke — clearing the
+ * data but not the loading flag left the page rendering neither a spinner nor
+ * a result, so it went blank for as long as the next fetch took.
+ *
+ * Keying the subtree is React's own answer to "reset all state when an input
+ * changes", and it cannot drift: new state is whatever the component starts
+ * with, so a value added later is covered without anybody remembering to reset
+ * it here.
+ *
+ * It reads the year on the client so the pages above it stay prerenderable;
+ * reading searchParams in the page itself would make the whole route dynamic.
+ */
+export function CohortScope({ children }: { children: ReactNode }) {
+ const cohort = cohortFromParam(useSearchParams().get("year"));
+ return {children};
+}