Skip to content

Comments

Refactor useUrlValueProvider#6269

Merged
witoszekdev merged 3 commits intomainfrom
eng-695-refactor-use-url-value-provider
Jan 27, 2026
Merged

Refactor useUrlValueProvider#6269
witoszekdev merged 3 commits intomainfrom
eng-695-refactor-use-url-value-provider

Conversation

@witoszekdev
Copy link
Member

@witoszekdev witoszekdev commented Jan 21, 2026

Refactored useUrlValueProvider so that it has clear explanation for the workaround we're doing with URL ↔ React state.

Removed unnecessary switch, since it execute the same code path anyway (used type assertion).

Added missing useCallback and useMemo that were causing warnings in ESLint

Found issue with it when working on: #6269

@witoszekdev witoszekdev requested a review from a team as a code owner January 21, 2026 16:34
@changeset-bot
Copy link

changeset-bot bot commented Jan 21, 2026

⚠️ No Changeset found

Latest commit: 368052d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov
Copy link

codecov bot commented Jan 21, 2026

Codecov Report

❌ Patch coverage is 4.44444% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.54%. Comparing base (b39710b) to head (368052d).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...itionalFilter/ValueProvider/useUrlValueProvider.ts 4.44% 42 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6269      +/-   ##
==========================================
+ Coverage   42.53%   42.54%   +0.01%     
==========================================
  Files        2486     2486              
  Lines       42955    42944      -11     
  Branches    10109    10061      -48     
==========================================
  Hits        18271    18271              
+ Misses      23386    23378       -8     
+ Partials     1298     1295       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the useUrlValueProvider hook to fix a race condition between URL changes and React state synchronization. The refactor introduces a UserOverride mechanism to distinguish between user-initiated filter changes and URL rehydration, removes a large switch statement in favor of a more generic approach using refs, and adds React optimization hooks (useCallback, useMemo).

Changes:

  • Introduced UserOverride interface to store user selections with URL snapshots to resolve race conditions
  • Replaced type-specific switch statement with a generic ref-based approach for calling fetchQueries
  • Wrapped callbacks and computed values in useCallback/useMemo for performance optimization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +75
fetchQueriesRef.current = initialState?.fetchQueries as
| ((params: typeof fetchingParams) => Promise<void>)
| null;
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion as ((params: typeof fetchingParams) => Promise<void>) | null is unsafe. Since initialState is a union type (InitialAPIState), each variant's fetchQueries accepts different parameter types. The fetchingParams type is computed based on the type prop, but TypeScript cannot verify at compile time that the correlation between type and initialState is correct.

If a mismatch occurs (e.g., type is "product" but initialState is for "order"), this will cause a runtime error when fetchQueries is called with incorrect parameters. Consider adding runtime validation to ensure type matches the initialState variant, or restructure the type system to make this relationship explicit at the type level.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +168
loading: initialState?.loading || false,
persist,
clear,
isPersisted,
getTokenByName,
count,
}),
[value, initialState?.loading, persist, clear, isPersisted, getTokenByName, count],
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return object is wrapped in useMemo with initialState?.loading as a dependency (line 168), but the loading variable extracted on line 63 (initialState?.loading ?? false) is not included in the dependencies. If initialState?.loading changes from undefined to false, the memoized object won't update because both evaluate to false, but the dependency array is comparing against initialState?.loading directly.

This inconsistency could cause stale values in the returned object. Either use loading as the dependency (and extract it before the useMemo), or consistently use initialState?.loading ?? false in both the return object and dependency array.

Suggested change
loading: initialState?.loading || false,
persist,
clear,
isPersisted,
getTokenByName,
count,
}),
[value, initialState?.loading, persist, clear, isPersisted, getTokenByName, count],
loading: initialState?.loading ?? false,
persist,
clear,
isPersisted,
getTokenByName,
count,
}),
[value, initialState?.loading ?? false, persist, clear, isPersisted, getTokenByName, count],

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +139
setUserOverride({
urlSnapshot: "",
value: [],
});
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clear function sets userOverride to have an empty urlSnapshot: "" and value: [] (lines 136-139). However, when the URL is actually cleared via router.history.replace() with no search params (lines 133-135), the filterQueryString will also become an empty string.

This means the check on line 97 (userOverride.urlSnapshot === filterQueryString) will match (both are empty strings), causing the system to use the user override's empty array instead of the rehydrated value. This prevents the filters from being properly rehydrated if they're later added back to the URL through browser navigation (back/forward buttons) or external URL changes.

Consider setting userOverride to null instead of an explicit empty override when clearing, so the system will use the rehydrated value from the URL.

Suggested change
setUserOverride({
urlSnapshot: "",
value: [],
});
setUserOverride(null);

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +122
const newUrlSnapshot = Object.entries(filterStructureParams)
.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`)
.join("&");
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL snapshot generation is incorrect. prepareStructure(filterValue) returns a Structure type which is Array<string | UrlEntry | Structure>, not a plain object. Calling Object.entries() on an array produces index-value pairs (e.g., ["0", firstElement], ["1", secondElement]), not the key-value pairs needed to reconstruct the query string.

This means the newUrlSnapshot will never match filterQueryString (line 97), breaking the race condition fix. The comparison on line 97 will always fail, causing the user override mechanism to not work as intended.

The snapshot should be generated using the same method as filterQueryString - either by using stringify() from 'qs' on the result of prepareStructure(), or by extracting the filter-related params from the URL after the router.history.replace() call.

Suggested change
const newUrlSnapshot = Object.entries(filterStructureParams)
.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`)
.join("&");
const newUrlSnapshot = stringify(filterStructureParams);

Copilot uses AI. Check for mistakes.
Comment on lines 78 to +82
useEffect(() => {
if (!initialState) return;
if (!fetchQueriesRef.current || !fetchingParams) return;

const { data, loading } = initialState;
fetchQueriesRef.current(fetchingParams);
}, [fetchingParams]);
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect has fetchingParams as a dependency, but fetchingParams is recalculated on every render (lines 57-60) because it depends on tokenizedUrl.getFetchingParams() which may return a new object reference each time. This could cause excessive API calls whenever the component re-renders, even if the actual fetching parameters haven't changed.

Consider memoizing fetchingParams or adding a deep equality check to prevent unnecessary fetches when the params haven't meaningfully changed.

Copilot uses AI. Check for mistakes.
@witoszekdev witoszekdev added the skip changeset Use if your changes doesn't need entry in changelog label Jan 21, 2026
@witoszekdev witoszekdev enabled auto-merge (squash) January 27, 2026 13:01
Copilot AI review requested due to automatic review settings January 27, 2026 15:02
@witoszekdev witoszekdev merged commit 944569d into main Jan 27, 2026
16 checks passed
@witoszekdev witoszekdev deleted the eng-695-refactor-use-url-value-provider branch January 27, 2026 15:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 58 to 60
const fetchingParams = paramsFromType
? tokenizedUrl.getFetchingParams(paramsFromType, type)
: null;
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetchingParams variable should be memoized with useMemo to prevent unnecessary re-execution of the fetch effect at lines 78-82. Currently, tokenizedUrl.getFetchingParams() returns a new object reference on every render, causing the effect to run on every render instead of only when the filter query string changes. This is a performance regression from the original implementation which only ran when locationSearch changed.

Wrap the computation in useMemo with dependencies on tokenizedUrl, paramsFromType, and type.

Suggested change
const fetchingParams = paramsFromType
? tokenizedUrl.getFetchingParams(paramsFromType, type)
: null;
const fetchingParams = useMemo(
() => (paramsFromType ? tokenizedUrl.getFetchingParams(paramsFromType, type) : null),
[tokenizedUrl, paramsFromType, type],
);

Copilot uses AI. Check for mistakes.
witoszekdev added a commit that referenced this pull request Jan 28, 2026
witoszekdev added a commit that referenced this pull request Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip changeset Use if your changes doesn't need entry in changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants