Skip to content

new: url primitive for Solid 2.0#978

Merged
davedbase merged 10 commits into
solidjs-community:nextfrom
davedbase:v2/url
Jul 7, 2026
Merged

new: url primitive for Solid 2.0#978
davedbase merged 10 commits into
solidjs-community:nextfrom
davedbase:v2/url

Conversation

@davedbase

@davedbase davedbase commented Jul 7, 2026

Copy link
Copy Markdown
Member

Attempts to provide a solution for #427 and #770

Adds @solid-primitives/url, adapted from the design proposed in the unmerged PR #77, rewritten from scratch for Solid 2.0:

  • createLocationState / useSharedLocationState / updateLocation / setLocationFallback — reactive window.location, with push/replace/navigate setters
  • createURL / ReactiveURL / createURLRecord — a granularly reactive URL-like object and store
  • createSearchParams / ReactiveSearchParams / createLocationSearchParams / useSharedLocationSearchParams / getSearchParamsRecord — a granularly reactive URLSearchParams-like object, standalone or bound to the browser's location
  • Rebuilt on current Solid-2.0-native primitives instead of the original PR's bespoke utilities: @solid-primitives/static-store for the shallow reactive records, @solid-primitives/trigger's TriggerCache for ReactiveSearchParams' granular per-key tracking, a createStore(fn, seed) projection for the dynamic-keyed SearchParamsRecord, and a guarded imperative two-way sync between .search and .searchParams (replacing the removed createComputed/on)
  • Fixed two Solid-2.0-specific correctness bugs surfaced while testing the two-way sync live in Storybook (not caught by the unit suite, since it doesn't render through a host framework's reactive graph): a memo-tainting leak from an unwrapped this.search read during ReactiveSearchParams's lazy construction, and a stale post-write read caused by Solid 2's write-batching once .search is bound reactively elsewhere
  • Adds Storybook stories (Browser APIs/URL) and a full test suite (33 tests: 28 client + 5 SSR)
  • README documents the API and calls out Solid Router as the better choice for apps that need routing anyway, positioning this package as the lightweight alternative
  • Registers the package in the root README's package table and adds a changeset (minor, initial release)

Summary by CodeRabbit

  • New Features

    • Added a new URL primitives package with reactive handling for location state, URL values, and search parameters.
    • Introduced shared and SSR-friendly helpers for working with browser location and query strings.
    • Added Storybook examples showcasing URL, location, and search-parameter reactivity.
  • Documentation

    • Updated the main catalogue and README to include the new URL primitives.
    • Added package-specific documentation and license information.
  • Tests

    • Added coverage for browser and server-side behavior, including reactivity and URL synchronization.

@davedbase davedbase added this to the Solid 2.0 Migration milestone Jul 7, 2026
@davedbase davedbase added the Primitive Proposal For discussing a primitive proposition and the API design label Jul 7, 2026
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e310c4f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solid-primitives/url Minor

Not sure what this means? Click here to learn what changesets are.

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

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cad5b2af-b173-4b42-8519-3d64d3923cb9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new @solid-primitives/url package providing reactive Solid.js primitives for window.location (createLocationState, updateLocation), native URL objects (createURL, createURLRecord, ReactiveURL), and URLSearchParams (createSearchParams, ReactiveSearchParams, createLocationSearchParams), with package scaffolding, tests, stories, and documentation, plus minor updates to the root README and analytics package metadata.

Changes

New @solid-primitives/url package

Layer / File(s) Summary
Package scaffolding and catalogue registration
packages/url/package.json, packages/url/tsconfig.json, packages/url/LICENSE, .changeset/url-initial.md, README.md, packages/analytics/package.json, packages/url/README.md (intro/changelog)
Adds package metadata, build config, license, changeset, root README catalogue/badge updates, an analytics gzip field, and the new package's README intro and changelog sections.
Shared types and public entrypoint
packages/url/src/common.ts, packages/url/src/index.ts
Defines the SetterValue type and re-exports location, search-params, and URL APIs as the package's public surface.
Reactive location state primitive
packages/url/src/location.ts, packages/url/test/location.test.ts, packages/url/test/server.test.ts, packages/url/stories/location.stories.tsx, packages/url/README.md
Implements createLocationState, updateLocation, setLocationFallback, and useSharedLocationState with history patching, SSR fallback handling, tests, an SSR test suite, and a Storybook demo.
Reactive search params primitive
packages/url/src/searchParams.ts, packages/url/test/searchParams.test.ts, packages/url/stories/searchParams.stories.tsx, packages/url/stories/locationSearchParams.stories.tsx, packages/url/README.md
Implements getSearchParamsRecord, ReactiveSearchParams (with granular reactive tracking), createSearchParams, createLocationSearchParams, and useSharedLocationSearchParams, with tests, Storybook demos, and docs.
Reactive URL primitive
packages/url/src/url.ts, packages/url/test/url.test.ts, packages/url/stories/url.stories.tsx, packages/url/README.md
Implements createURLRecord, createURL, and ReactiveURL with two-way .search/searchParams synchronization, tests, Storybook demos, and docs.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Component
  participant createLocationState
  participant History
  participant Window

  Component->>createLocationState: push/replace/navigate(fields)
  createLocationState->>createLocationState: build URL from location.href
  createLocationState->>History: pushState/replaceState (or set location.href)
  History-->>Window: popstate/hashchange
  Window-->>createLocationState: notify listeners
  createLocationState-->>Component: update reactive LocationState store
Loading
sequenceDiagram
  participant Component
  participant ReactiveURL
  participant ReactiveSearchParams

  Component->>ReactiveURL: set search = "?a=1"
  ReactiveURL->>ReactiveURL: syncSearchParamsFromSearch()
  ReactiveURL->>ReactiveSearchParams: apply diffed entries
  Component->>ReactiveSearchParams: set("a", "2")
  ReactiveSearchParams-->>ReactiveURL: onChange callback
  ReactiveURL->>ReactiveURL: update .search from searchParams.toString()
Loading

Suggested labels: solid-2.0

Suggested reviewers: atk, brenelz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new URL primitive package for Solid 2.0.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@davedbase davedbase requested a review from thetarnav July 7, 2026 00:40
@davedbase davedbase mentioned this pull request Jul 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
packages/url/test/location.test.ts (1)

59-65: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider a regression test for the (key, value) overload with origin.

Only the record form (push({ origin: ..., pathname: ... })) is tested for the origin-immutability guarantee (Lines 75-83). Given the asymmetry noted in location.ts's setter (record branch guards origin, key/value branch doesn't), a test like push("origin", "http://evil.com") would have caught that gap.

Also applies to: 75-83

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/url/test/location.test.ts` around lines 59 - 65, Add a regression
test for the createLocationState push setter’s key/value overload to cover
origin immutability, since only the record overload currently verifies that
behavior. In location.test.ts, extend the existing push-related tests around
createLocationState and push so that calling push("origin", "http://evil.com")
is asserted to leave the state.origin unchanged, matching the protection already
tested in the record-form branch of the setter.
packages/url/test/searchParams.test.ts (1)

27-91: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing regression test for sort().

None of the ReactiveSearchParams tests exercise .sort(). As flagged in searchParams.ts (lines 196-199), sort() skips the onChange notification that other mutators call, which — once fixed — should be locked in with a test (e.g. asserting toString() reflects the sorted order and that a WHOLE-tracking effect reruns).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/url/test/searchParams.test.ts` around lines 27 - 91, Add a
regression test in ReactiveSearchParams coverage for sort() to lock in the
missing onChange behavior. Use ReactiveSearchParams plus createEffect/flush in
the test suite to assert that calling sort() updates toString() into sorted
order and reruns a WHOLE-tracking effect, mirroring how other mutators are
tested.
packages/url/test/url.test.ts (1)

78-130: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding a sort() sync test here too.

Given sort() is a mutator that should re-serialize onto url.search on a native URL (per MDN, any searchParams update — including sort — re-serializes onto .search), this searchParams describe block would be a natural place for a regression test once the sort()/onChange bug in searchParams.ts is fixed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/url/test/url.test.ts` around lines 78 - 130, Add a regression test
in the searchParams describe block for the missing sort() sync behavior. Use
createURL, url.searchParams.sort(), and flush() to verify that calling sort()
re-serializes back onto url.search and keeps searchParams and url.search in
sync, similar to the existing mutator tests in url.test.ts.
packages/url/src/searchParams.ts (2)

133-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Class JSDoc gets detached by the WHOLE symbol declaration.

The doc comment intended for ReactiveSearchParams (lines 133-148) is immediately followed by WHOLE's own doc comment and declaration (149-150), then a blank line before export class (152). Editors associate a JSDoc with the next declaration only when adjacent — as written, the class's real doc-comment won't show up in IDE tooltips/hover for ReactiveSearchParams.

♻️ Suggested reordering
-/**
- * A reactive version of the `URLSearchParams` class. Every read method is granular — it causes
- * an update only when the value it read has actually changed.
- * ...
- */
-/** Tracking key standing in for "any part of the collection" — reads that iterate the whole thing (`toString`, `keys`, `entries`, `values`, `forEach`) depend on it, and every mutation dirties it. */
-const WHOLE = Symbol("whole");
-
-export class ReactiveSearchParams extends URLSearchParams {
+/** Tracking key standing in for "any part of the collection" — reads that iterate the whole thing (`toString`, `keys`, `entries`, `values`, `forEach`) depend on it, and every mutation dirties it. */
+const WHOLE = Symbol("whole");
+
+/**
+ * A reactive version of the `URLSearchParams` class. Every read method is granular — it causes
+ * an update only when the value it read has actually changed.
+ * ...
+ */
+export class ReactiveSearchParams extends URLSearchParams {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/url/src/searchParams.ts` around lines 133 - 151, The
`ReactiveSearchParams` JSDoc is being separated from the class by the `WHOLE`
symbol comment/declaration, so move the `WHOLE` tracking-key comment and
declaration to stay with `WHOLE` and keep the `ReactiveSearchParams` doc block
immediately adjacent to the `export class ReactiveSearchParams` declaration.
Ensure the class’s JSDoc directly precedes `ReactiveSearchParams`, and place the
`WHOLE` documentation right above `const WHOLE = Symbol("whole")` so IDE
hover/tooltips attach correctly.

185-189: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

delete/has overrides drop the spec's optional value parameter.

Per MDN, delete() and has() on native URLSearchParams both accept an optional second value argument to match a specific name-value pair rather than all entries with that name: delete(name) delete(name, value) name · The name of the parameters to be deleted. value Optional · The value that parameters must match, along with the given name, to be deleted. The overrides here only accept name: string, so calling searchParams.delete("foo", "bar") or .has("foo", "bar") on a ReactiveSearchParams silently ignores the value filter — a behavioral divergence from the class's stated goal of behaving "like URLSearchParams". Worth confirming whether the target runtime/browser matrix for this package needs to support this and, if so, extending the overrides to accept and forward the optional value.

Also applies to: 210-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/url/src/searchParams.ts` around lines 185 - 189, The
ReactiveSearchParams overrides for delete and has are missing the native
URLSearchParams optional value argument, so calls that target a specific
name-value pair behave incorrectly. Update the delete and has methods in
ReactiveSearchParams to accept the optional second value parameter and forward
it to the underlying super implementation, preserving the existing notify
behavior after mutations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/url/src/location.ts`:
- Around line 149-168: The single-key path in the setter overload allows
assigning to `origin` without a runtime guard, unlike the record branch that
already skips it. Update the `setter` logic in `location.ts` so the `typeof a
=== "string"` branch also rejects or ignores `"origin"` before writing to
`url[a]`, matching the existing protection in the `(key, value)` record handling
and preventing the strict-mode throw from `URL.prototype.origin`.
- Line 43: The location fallback and shared location state are currently
process-wide singletons, which can leak state across concurrent SSR requests.
Update the location handling in location.ts so `setLocationFallback` is not
stored in a module-level variable shared by the whole Node process, and replace
`useSharedLocationState`’s `createSingletonRoot`-based behavior with
`createHydratableSingletonRoot` or restrict it from server rendering. Keep the
fix centered around the `LocationState`, `setLocationFallback`, and
`useSharedLocationState` symbols so the state is request-safe during SSR.

In `@packages/url/src/searchParams.ts`:
- Around line 196-199: The sort() override in ReactiveURLSearchParams currently
only marks the whole object dirty and bypasses the change notification path,
which leaves ReactiveURL.search and href out of sync after
url.searchParams.sort(). Update sort() to use the existing `#notify`() flow
instead of calling `#dirty`(WHOLE) directly, so the onChange callback runs and the
parent URL state stays synchronized. Add a regression test around
ReactiveURLSearchParams.sort() to verify the URL search string and href update
correctly after sorting.
- Around line 161-172: The ReactiveSearchParams constructor currently copies
from an existing instance by calling init.toString() directly, which creates an
unintended tracked read on the source object's WHOLE trigger. Wrap the copy path
in untrack so constructing from another ReactiveSearchParams performs a
one-time, dependency-free read, and apply this fix in the constructor of
ReactiveSearchParams (with the needed solid-js untrack import).

---

Nitpick comments:
In `@packages/url/src/searchParams.ts`:
- Around line 133-151: The `ReactiveSearchParams` JSDoc is being separated from
the class by the `WHOLE` symbol comment/declaration, so move the `WHOLE`
tracking-key comment and declaration to stay with `WHOLE` and keep the
`ReactiveSearchParams` doc block immediately adjacent to the `export class
ReactiveSearchParams` declaration. Ensure the class’s JSDoc directly precedes
`ReactiveSearchParams`, and place the `WHOLE` documentation right above `const
WHOLE = Symbol("whole")` so IDE hover/tooltips attach correctly.
- Around line 185-189: The ReactiveSearchParams overrides for delete and has are
missing the native URLSearchParams optional value argument, so calls that target
a specific name-value pair behave incorrectly. Update the delete and has methods
in ReactiveSearchParams to accept the optional second value parameter and
forward it to the underlying super implementation, preserving the existing
notify behavior after mutations.

In `@packages/url/test/location.test.ts`:
- Around line 59-65: Add a regression test for the createLocationState push
setter’s key/value overload to cover origin immutability, since only the record
overload currently verifies that behavior. In location.test.ts, extend the
existing push-related tests around createLocationState and push so that calling
push("origin", "http://evil.com") is asserted to leave the state.origin
unchanged, matching the protection already tested in the record-form branch of
the setter.

In `@packages/url/test/searchParams.test.ts`:
- Around line 27-91: Add a regression test in ReactiveSearchParams coverage for
sort() to lock in the missing onChange behavior. Use ReactiveSearchParams plus
createEffect/flush in the test suite to assert that calling sort() updates
toString() into sorted order and reruns a WHOLE-tracking effect, mirroring how
other mutators are tested.

In `@packages/url/test/url.test.ts`:
- Around line 78-130: Add a regression test in the searchParams describe block
for the missing sort() sync behavior. Use createURL, url.searchParams.sort(),
and flush() to verify that calling sort() re-serializes back onto url.search and
keeps searchParams and url.search in sync, similar to the existing mutator tests
in url.test.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8bf75d35-c961-4858-93bf-2bbc27752024

📥 Commits

Reviewing files that changed from the base of the PR and between 734491c and e5eed68.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • .changeset/url-initial.md
  • README.md
  • packages/analytics/package.json
  • packages/url/LICENSE
  • packages/url/README.md
  • packages/url/package.json
  • packages/url/src/common.ts
  • packages/url/src/index.ts
  • packages/url/src/location.ts
  • packages/url/src/searchParams.ts
  • packages/url/src/url.ts
  • packages/url/stories/location.stories.tsx
  • packages/url/stories/locationSearchParams.stories.tsx
  • packages/url/stories/searchParams.stories.tsx
  • packages/url/stories/url.stories.tsx
  • packages/url/test/location.test.ts
  • packages/url/test/searchParams.test.ts
  • packages/url/test/server.test.ts
  • packages/url/test/url.test.ts
  • packages/url/tsconfig.json

Comment thread packages/url/src/location.ts
Comment thread packages/url/src/location.ts
Comment thread packages/url/src/searchParams.ts
Comment thread packages/url/src/searchParams.ts
Comment thread packages/url/src/common.ts Outdated
Comment thread packages/url/src/location.ts Outdated
Comment thread packages/url/test/location.test.ts Outdated
Comment thread packages/url/test/searchParams.test.ts Outdated
Comment thread packages/url/LICENSE Outdated
@davedbase davedbase merged commit b64c433 into solidjs-community:next Jul 7, 2026
1 of 2 checks passed
@davedbase davedbase deleted the v2/url branch July 7, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Primitive Proposal For discussing a primitive proposition and the API design

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants