Skip to content

Commit a778196

Browse files
authored
feat: sync route params with compare form inputs (#119)
* fix(home): resync form and results to URL on back/forward navigation Round-2 review: the previous version only consumed search params on initial mount, so router.push from compare/swap/reset put history entries that the back button couldn't restore. Now a useEffect watches searchParams and resyncs username1/username2 + triggers a re-fetch when the URL pair differs from the last fetched pair. lastFetchedPairRef short-circuits the loop with our own router.push. * fix(compare-form): accept username state as props The PR call site lifted username1/username2 and their setters into HomePageInner so URL searchParams could drive the form, but the component itself still owned its own useState pair. That broke the build and meant the prop-passed values were ignored at runtime. Move the four values into CompareFormProps, drop the internal useState, and let parent-supplied swapUsers/reset handle clearing and swapping. The local placeholder defaults ("pbiggar", "CoralineAda") are gone; the parent now seeds initial state from searchParams. --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
1 parent 9f85f15 commit a778196

2 files changed

Lines changed: 81 additions & 10 deletions

File tree

app/page.tsx

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { useMemo, useState } from "react";
3+
import { Suspense, useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
4+
import { useRouter, useSearchParams } from "next/navigation";
45
import { CompareForm } from "../components/compare-form";
56
import { ResultDashboard } from "../components/result-dashboard";
67
import { DashboardSkeleton } from "../components/skeletons";
@@ -16,14 +17,24 @@ type ApiResponse = {
1617
error?: string;
1718
};
1819

19-
export default function HomePage() {
20+
function HomePageInner() {
2021
const { t } = useTranslation();
22+
const router = useRouter();
23+
const searchParams = useSearchParams();
24+
const initialUsernames = searchParams.getAll("username");
25+
const initialUsername1 = initialUsernames[0] ?? "";
26+
const initialUsername2 = initialUsernames[1] ?? "";
2127
const [loading, setLoading] = useState(false);
2228
const [error, setError] = useState<string | null>(null);
29+
const [username1, setUsername1] = useState(initialUsername1);
30+
const [username2, setUsername2] = useState(initialUsername2);
2331
const [data, setData] = useState<{
2432
user1: UserResult;
2533
user2: UserResult;
2634
} | null>(null);
35+
// Track the URL pair we last fetched against so back/forward navigation
36+
// can resync the form and results without re-fetching identical pairs.
37+
const lastFetchedPairRef = useRef<[string, string] | null>(null);
2738

2839
const localizeErrorMessage = (message?: string) => {
2940
switch (message) {
@@ -43,6 +54,11 @@ export default function HomePage() {
4354
};
4455

4556
const handleCompare = async (u1: string, u2: string) => {
57+
lastFetchedPairRef.current = [u1, u2];
58+
router.push(
59+
`/?username=${encodeURIComponent(u1)}&username=${encodeURIComponent(u2)}`,
60+
{ scroll: false }
61+
);
4662
setLoading(true);
4763
setError(null);
4864
setData(null);
@@ -79,14 +95,56 @@ export default function HomePage() {
7995
}
8096
};
8197

98+
// Resync form + results to whatever the URL says — handles initial mount
99+
// AND back/forward navigation. We fetch only when the URL pair differs from
100+
// the last pair we fetched, so no infinite loop with the router.push above.
101+
const syncToUrl = useEffectEvent((u1: string, u2: string) => {
102+
setUsername1(u1);
103+
setUsername2(u2);
104+
105+
if (!u1 || !u2) {
106+
// Empty params: clear results so the empty state matches the URL.
107+
lastFetchedPairRef.current = null;
108+
setData(null);
109+
setError(null);
110+
return;
111+
}
112+
113+
const last = lastFetchedPairRef.current;
114+
if (last && last[0] === u1 && last[1] === u2) {
115+
// URL already reflects the most recent fetch; nothing to do.
116+
return;
117+
}
118+
119+
void handleCompare(u1, u2);
120+
});
121+
122+
useEffect(() => {
123+
const params = searchParams.getAll("username");
124+
syncToUrl(params[0] ?? "", params[1] ?? "");
125+
}, [searchParams]);
126+
82127
const skeleton = useMemo(() => <DashboardSkeleton />, []);
83128

84129
const reset = () => {
85130
setData(null);
86131
setError(null);
132+
setUsername1("");
133+
setUsername2("");
134+
router.push("/", { scroll: false });
87135
};
88136

89137
const swapUsers = () => {
138+
const nextUsername1 = username2;
139+
const nextUsername2 = username1;
140+
141+
setUsername1(nextUsername1);
142+
setUsername2(nextUsername2);
143+
router.push(
144+
`/?username=${encodeURIComponent(nextUsername1)}&username=${encodeURIComponent(nextUsername2)}`,
145+
{ scroll: false }
146+
);
147+
90148
if (!data) return;
91149
setData((d) => ({ user1: d!.user2, user2: d!.user1 }));
92150
};
@@ -97,6 +155,10 @@ export default function HomePage() {
97155

98156
<div className="w-full flex-1 max-w-6xl mx-auto px-4 py-10 space-y-6">
99157
<CompareForm
158+
username1={username1}
159+
username2={username2}
160+
setUsername1={setUsername1}
161+
setUsername2={setUsername2}
100162
onSubmit={handleCompare}
101163
loading={loading}
102164
reset={reset}
@@ -126,3 +188,11 @@ export default function HomePage() {
126188
</main>
127189
);
128190
}
191+
192+
export default function HomePage() {
193+
return (
194+
<Suspense fallback={<DashboardSkeleton />}>
195+
<HomePageInner />
196+
</Suspense>
197+
);
198+
}

components/compare-form.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useRef, useEffect } from "react";
1+
import { useRef, useEffect } from "react";
22
import { Button } from "./ui/button";
33
import { ArrowLeftRight, RefreshCw } from "lucide-react";
44
import {
@@ -12,6 +12,10 @@ import { Alert, AlertDescription } from "./ui/alert";
1212
import { useTranslation } from "./language-provider";
1313

1414
type CompareFormProps = {
15+
username1: string;
16+
username2: string;
17+
setUsername1: (value: string) => void;
18+
setUsername2: (value: string) => void;
1519
data?: boolean;
1620
onSubmit: (u1: string, u2: string) => void;
1721
loading?: boolean;
@@ -21,16 +25,17 @@ type CompareFormProps = {
2125
};
2226

2327
export function CompareForm({
28+
username1,
29+
username2,
30+
setUsername1,
31+
setUsername2,
2432
onSubmit,
25-
data,
2633
loading,
2734
swapUsers,
2835
reset,
2936
error,
3037
}: CompareFormProps) {
3138
const { t } = useTranslation();
32-
const [username1, setUsername1] = useState("pbiggar");
33-
const [username2, setUsername2] = useState("CoralineAda");
3439
const firstInputRef = useRef<HTMLInputElement>(null);
3540

3641
// Auto-focus first input on page load
@@ -42,14 +47,10 @@ export function CompareForm({
4247
const isEmpty = (!username1.trim() && !username2.trim()) && !data;
4348

4449
const handleSwap = () => {
45-
setUsername1(username2);
46-
setUsername2(username1);
4750
if (swapUsers) swapUsers();
4851
};
4952

5053
const handleReset = () => {
51-
setUsername1("");
52-
setUsername2("");
5354
if (reset) reset();
5455
};
5556

0 commit comments

Comments
 (0)