Skip to content

feat(core): notify, i18n, and useListState primitives (#131, #132, #133)#140

Merged
olliethedev merged 7 commits into
mainfrom
feat/131-133-core-primitives
Jul 5, 2026
Merged

feat(core): notify, i18n, and useListState primitives (#131, #132, #133)#140
olliethedev merged 7 commits into
mainfrom
feat/131-133-core-primitives

Conversation

@olliethedev

@olliethedev olliethedev commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Core provider primitives for the Refine-inspired simplification effort — plugin adoption is intentionally deferred to #136.

Part of #131
Part of #132
Part of #133

Test plan

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm test -- --run
  • CI green

Note

Medium Risk
Touches StackProvider composition and global URL/history batching in useListState, which can affect navigation and back-button behavior across list UIs; defaults preserve prior behavior when new props are omitted.

Overview
Introduces core provider primitives for notifications, i18n, and URL-backed list UI state. Plugin wiring is intentionally left for a follow-up (#136).

StackProvider now accepts optional notify and i18n props, wrapped in new boundaries around the existing router/auth tree. useNotify() merges partial overrides with sonner defaults. useTranslate() calls a custom translate when provided, otherwise returns English defaults with {{param}} interpolation via shared interpolate.

useListState (exported from @btst/stack/client) syncs filters/tabs/pagination to the router’s getSearchParams / setSearchParams, omits default values from the URL, supports per-field history: "replace"|"push, batches same-tick updates into one navigation, skips no-op history writes, and uses a module store so sibling hook instances stay in sync. SSR can seed state with parseListStateFromSearchParams.

Also adds extract-i18n-keys.ts for future plugin sweeps, API reference sections for the new hooks, and Vitest coverage for providers, list-state serialization, and hook/router behavior.

Reviewed by Cursor Bugbot for commit 12a2313. Bugbot is set up for automated code reviews on this repo. Configure here.

Introduce StackProvider notify/i18n props with useNotify() and
useTranslate() hooks, plus URL-synced useListState for list pages.
Router search params were already on StackRouter from #127.

Part of #131, #132, #133. Plugin adoption deferred to #136.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-stack-docs Ready Ready Preview, Comment Jul 5, 2026 8:51pm
better-stack-playground Ready Ready Preview, Comment Jul 5, 2026 8:51pm

Request Review

Comment thread packages/stack/src/client/hooks/use-list-state.ts
Comment thread packages/stack/src/client/hooks/use-list-state.ts

@cursor cursor 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.

Security Review — No High-Confidence Vulnerabilities Found

Reviewed the full diff for PR #140 (notify, i18n, and useListState primitives). No confirmed vulnerabilities were found. Below are the areas examined and the reasoning.


Checklist

Area Finding
Injection (SQL, command, template, path traversal) ✅ Clean
Authn/authz bypasses ✅ Not applicable (no auth surface changed)
Secrets / token leakage / insecure logging ✅ Clean
Unsafe deserialization / SSRF / XSS / CSRF ✅ Clean
Dependency / supply-chain risk ✅ No new dependencies introduced

Detailed Notes

shared/interpolate.ts{{param}} template engine

The regex \{\{(\w+)\}\} restricts keys to [a-zA-Z0-9_]+. Keys like __proto__ and constructor match \w+, but the access is read-only (params[key]String(value)). There is no write path, so prototype pollution is not possible through this function. Rendered output is always a plain string; React's JSX renderer escapes it before display, so no XSS concern within the library's own UI.

shared/list-state.ts — URL search-param parsing

parseListStateFromSearchParams returns raw URL string values for type: "string" schema fields with no sanitization. This is appropriate for a client-side URL-state utility — React escapes strings at the JSX render boundary. Consumers who pass these values to dangerouslySetInnerHTML or other raw-HTML sinks would be responsible for their own sanitization, which is standard React practice and not a library-level vulnerability.

context/notify.tsx — notification provider

Messages are forwarded as strings to sonner's toast.* APIs (or a consumer-supplied override). Sonner renders toast content as React children, not raw HTML, so there is no XSS vector in the default path.

scripts/extract-i18n-keys.ts — dev-only file scanner

The script walks a hardcoded path derived from import.meta.dirname (resolved at build time) and emits JSON to stdout. It is a local developer tool, not deployed or invoked by end users. No injection or path-traversal risk.

No new dependencies were added to package.json; sonner was already a declared dependency.


Verdict: No actionable security findings. This PR is clear to merge from a security standpoint.

Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities

Co-authored-by: Cursor <cursoragent@cursor.com>
Address Bugbot feedback: derive next state from getSearchParams()
instead of a stale render closure, and re-read params each render so
router-driven URL changes are not masked by useMemo caching.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/stack/src/client/hooks/use-list-state.ts Outdated
Multiple setState calls in one event now merge into a single
setSearchParams call via microtask batching, and no-op updates skip
history entirely, so one back press undoes one user action.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/stack/src/client/hooks/use-list-state.ts Outdated
Move the coalescing queue to module scope so concurrent updates from
different useListState instances merge into one setSearchParams call
instead of the later flush clobbering the earlier one's params.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/stack/src/client/hooks/use-list-state.ts
…detection

URLSearchParams serialization is order-sensitive and the URL may hold
explicit default values, so string equality could miss state-identical
updates and push spurious history entries. Compare parsed field values
per entry instead, and use order-insensitive comparison for the final
net-no-op check.

Part of #133

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 75a1785. Configure here.

Comment thread packages/stack/src/client/hooks/use-list-state.ts Outdated
…L update

Replace per-instance urlVersion state with a module-level store consumed
via useSyncExternalStore. Previously only the instances that called
setState re-rendered after a flush, so sibling hooks sharing query keys
could serve stale state when the router binding does not re-render on
search-param changes (e.g. the Next.js preset). The popstate listener
moves into the shared store subscription.

Part of #133

Co-authored-by: Cursor <cursoragent@cursor.com>
@olliethedev olliethedev merged commit 89cc62f into main Jul 5, 2026
8 checks passed
@olliethedev olliethedev deleted the feat/131-133-core-primitives branch July 5, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant