Skip to content

Commit 2e9e996

Browse files
committed
Fix jobs detail navigation race
On slower connections, opening a job from a filtered jobs list could bounce back to /jobs/. JobSearch was re-emitting debounced filter updates on unrelated rerenders, and the jobs route treated those callbacks as replace navigations even when search params were unchanged. That could override an in-flight transition to /jobs/$jobId. The filter parser result is now memoized in useFilterInput so onFiltersChange is not retriggered when input text has not changed. The jobs route now compares derived filter search params against current params and skips navigation when the update is a no-op. A regression test was added to JobSearch to ensure an unrelated rerender does not notify the parent of filter changes. Closes #495.
1 parent 57a745d commit 2e9e996

3 files changed

Lines changed: 54 additions & 3 deletions

File tree

src/components/job-search/JobSearch.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,26 @@ describe("JobSearch", () => {
135135
}),
136136
]);
137137
});
138+
139+
it("does not notify parent on unrelated rerenders", async () => {
140+
const onFiltersChange = vi.fn();
141+
const { rerender } = render(
142+
<JobSearch onFiltersChange={onFiltersChange} />,
143+
);
144+
145+
await act(async () => {
146+
await new Promise((resolve) => setTimeout(resolve, 250));
147+
});
148+
onFiltersChange.mockClear();
149+
150+
rerender(<JobSearch onFiltersChange={onFiltersChange} />);
151+
152+
await act(async () => {
153+
await new Promise((resolve) => setTimeout(resolve, 250));
154+
});
155+
156+
expect(onFiltersChange).not.toHaveBeenCalled();
157+
});
138158
});
139159

140160
describe("Suggestion System", () => {

src/components/job-search/hooks.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useRef, useState } from "react";
1+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22

33
import {
44
analyzeAutocompleteContext,
@@ -50,7 +50,10 @@ export function useFilterInput({
5050
} | null>(null);
5151

5252
const debounceTimeoutRef = useRef<number | undefined>(undefined);
53-
const currentFilters = parseFiltersFromText(inputValue);
53+
const currentFilters = useMemo(
54+
() => parseFiltersFromText(inputValue),
55+
[inputValue],
56+
);
5457

5558
const {
5659
clearSuggestions,

src/routes/jobs/index.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const minimumLimit = 20;
3232
const defaultLimit = 20;
3333
const maximumLimit = 200;
3434

35+
const areStringArraysEqual = (a?: string[], b?: string[]) => {
36+
if (a === b) return true;
37+
if (!a && !b) return true;
38+
if (!a || !b) return false;
39+
if (a.length !== b.length) return false;
40+
return a.every((value, index) => value === b[index]);
41+
};
42+
3543
export const Route = createFileRoute("/jobs/")({
3644
validateSearch: jobSearchSchema,
3745
// Strip default values from URLs and retain important params across navigation
@@ -165,6 +173,26 @@ function JobsIndexComponent() {
165173
}
166174
});
167175

176+
const currentSearchParams = {
177+
id: id?.map(String),
178+
kind,
179+
priority: priority?.map(String),
180+
queue,
181+
};
182+
183+
// Avoid no-op navigations that can race with route transitions.
184+
if (
185+
areStringArraysEqual(currentSearchParams.id, searchParams.id) &&
186+
areStringArraysEqual(currentSearchParams.kind, searchParams.kind) &&
187+
areStringArraysEqual(
188+
currentSearchParams.priority,
189+
searchParams.priority,
190+
) &&
191+
areStringArraysEqual(currentSearchParams.queue, searchParams.queue)
192+
) {
193+
return;
194+
}
195+
168196
// Update route search params, preserving other existing ones
169197
navigate({
170198
replace: true,
@@ -179,7 +207,7 @@ function JobsIndexComponent() {
179207
},
180208
});
181209
},
182-
[navigate],
210+
[id, kind, navigate, priority, queue],
183211
);
184212

185213
// Convert current search params to initial filters

0 commit comments

Comments
 (0)