-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat(utils): Add useQueryStateWithLocalStorage hook #106625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat(utils): Add useQueryStateWithLocalStorage hook #106625
Conversation
…orage state sync This hook combines Nuqs (URL query parameters) with localStorage to provide persistent state that can also be shared via URL. Features: - URL query parameter takes precedence (enables sharing) - Falls back to localStorage when URL param is absent - Falls back to default value if neither exists - Single setter updates both URL and localStorage - Automatic localStorage key construction: namespace:key Use cases: - User preferences that should persist across sessions - Filters/sort options that should be shareable via URL - Any state that benefits from both persistence and shareability Example usage: ```typescript const [sortBy, setSortBy] = useQueryStateWithLocalStorage( 'releaseFilterSort', // URL param name 'dashboards', // localStorage namespace 'date' // default value ); ``` This creates: - URL param: ?releaseFilterSort=date - localStorage key: dashboards:releaseFilterSort Tests: - 7 comprehensive test cases covering all scenarios - Uses official Nuqs testing adapter - Verifies URL/localStorage sync behavior
Change from three positional arguments to a single props object for better
clarity and easier future extension.
Before:
```typescript
useQueryStateWithLocalStorage(key, namespace, defaultValue)
```
After:
```typescript
useQueryStateWithLocalStorage({key, namespace, defaultValue})
```
Benefits:
- Named parameters improve readability
- Order doesn't matter
- Easier to add optional parameters in the future
- More consistent with modern React patterns
Extend the hook to support any data type via Nuqs parsers, matching the
Nuqs API where parser is optional and defaults to parseAsString.
Changes:
- Add optional 'parser' parameter that accepts any Nuqs Parser<T>
- Defaults to parseAsString for backward compatibility
- Remove 'T extends string' constraint - now supports any type
- Parser handles URL serialization/deserialization
- localStorage uses JSON for storage (supports all JS types)
- Update docs with examples for string, integer, and boolean types
- Update tests: some use default parser, others explicitly test types
- Add 3 new tests for integer and boolean parsers
Example usage:
```typescript
// Strings (parser optional, defaults to parseAsString)
const [sort, setSort] = useQueryStateWithLocalStorage({
key: 'sort',
namespace: 'dashboards',
defaultValue: 'date',
});
// Integers (explicit parser)
const [pageSize, setPageSize] = useQueryStateWithLocalStorage({
key: 'pageSize',
namespace: 'dashboards',
defaultValue: 50,
parser: parseAsInteger,
});
// Booleans (explicit parser)
const [enabled, setEnabled] = useQueryStateWithLocalStorage({
key: 'enabled',
namespace: 'dashboards',
defaultValue: false,
parser: parseAsBoolean,
});
```
All 10 tests passing ✓
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| const setValue = (value: T) => { | ||
| setUrlValue(value); | ||
| setLocalStorageValue(value); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unstable setValue function causes infinite loops in consumers
High Severity
The setValue function is defined inline without useCallback, creating a new function reference on every render. This breaks React's referential equality expectations for setter functions. Consumers using setValue in dependency arrays (e.g., useEffect, useCallback, useMemo) will trigger on every render, potentially causing infinite loops or excessive re-renders.
| if (urlValue !== null && urlValue !== undefined && urlValue !== localStorageValue) { | ||
| setLocalStorageValue(urlValue); | ||
| } | ||
| }, [urlValue, localStorageValue, setLocalStorageValue]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reference equality check fails for arrays and objects
Medium Severity
The comparison urlValue !== localStorageValue uses strict reference equality, which fails for arrays and objects. When using parseAsArrayOf (documented as supported), two arrays with identical contents but different references will always compare as unequal. This causes unnecessary localStorage writes on every render where URL and localStorage have matching array values, and could cause infinite re-render loops if the URL parser returns new array instances on each render.
Adds a reusable hook that syncs state between URL query parameters and localStorage. URL takes precedence (for sharing), with localStorage providing session persistence.
Supports any data type via Nuqs parsers (strings, integers, booleans, arrays, etc.).
API
Creates URL param (e.g.,
?sort=date) and localStorage key (e.g.,dashboards:sort).Use Cases