README, docs and examples glow-up + react-rx-best-practices agent skill - #485
README, docs and examples glow-up + react-rx-best-practices agent skill#485stipsan wants to merge 26 commits into
Conversation
Fix HTTP method label
Fix delay unit lebel from seconds to milliseconds
Fix view transitions from blocking interactions for static list items
A best-practices skill (not a migration skill) for writing and reviewing React components that consume RxJS observables. Organized around idioms and anti-patterns: hand-rolled useState+useEffect+subscribe bridges, hook selection with per-hook "when not to use" lists, referential stability (incl. React Compiler detection), deriving state in the stream, preferring explicit Subjects over useObservableEvent, and the cases where a manual subscription is the right design. Authoritative source in .agents/skills/ with a .claude/skills symlink, matching the existing skill layout, so it is installable via `npx skills add sanity-io/react-rx --skill react-rx-best-practices`. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
- Lead with what react-rx is for: live streams, time, resilient data fetching, and how streams become React state that cooperates with concurrent rendering - Add a before/after of the hand-rolled subscription bridge - Feature list now covers the concurrent-React story: deferred identity-coherent updates, <Activity> pre-rendering, SSR, React Compiler-tested suite - Add a named Suspense section explaining useObservablePromise + use() and why useObservable is safe to suspend on (closes #56 material) - Add real-world use cases linking to the docs examples - Add an honest comparison table vs observable-hooks, @react-rxjs/core and DIY useEffect, plus a link to the cross-paradigm comparison page - Add a Used in production section: Sanity Studio (with adoption PRs), the plugin ecosystem, and navikt/aksel (closes #38 material) - Document the react-rx-best-practices agent skill install - Add npm weekly downloads badge Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
- index.md: mirror the README pitch — concurrent-React features, real-world use cases linking into examples, used-in-production, and an Agent skill section (flows into llms-full.txt) - New comparison page: RxJS-bindings table (observable-hooks, @react-rxjs/core, DIY) plus an async-state landscape table (Zustand, Jotai, XState, TanStack Query) framed as complementary tools - guide.md: new mental-model intro, expanded which-hook guidance, a 'Working with events' section teaching the explicit Subject pattern, and a Patterns section (retry via BehaviorSubject, errors as boundary or value, referential stability incl. React Compiler) - reference.mdx: every hook now has a 'When not to use' section; useObservableEvent documents the preferred explicit-Subject alternative; doc snippets no longer model useObservableEvent - llms.txt: dedicated Agent skill section with install command, registry link, and in-repo source path Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Inject Pico CSS (fluid class-less variant) into every preview iframe via Sandpack's externalResources — it never touches the sandbox bundler and keeps example sources free of styling imports and class names; semantic HTML looks designed automatically, with built-in light/dark. A hidden /index.tsx entry renders the example's default export and imports a hidden /styles.css with narrow-pane tuning: the preview pane is ~34% of the docs column (~280-360px), so the type scale and spacing shrink and pre/code/table get overflow guardrails. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Nobody uses observables for bouncing-ball animations in production — replace that example with the scenarios library consumers actually have: - llm-chat: a mock LLM chat where three conversations stream tokens concurrently. The mock (llm.ts) is clearly separated from the userland code (chat.ts, App.tsx). Visited chats stay mounted behind <Activity mode="hidden"> and reveal fully caught up; hovering an unvisited chat warms it with preloadObservablePromise; reads go through useObservablePromise + use() so tokens stream in place without re-suspending - relative-time: every "42s ago" label derives from one shared timer — one interval and one subscription for the whole list, no effects - resilient-fetch: polling that retries with exponential backoff, pauses while offline, resumes immediately on reconnect, and keeps the last good value visible throughout Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
- todo-app, search, form-data, suspense, fetch: events now push into Subjects from plain handlers instead of useObservableEvent, matching the pattern the docs recommend - form-data: fix the save stream never flattening its inner observable (map -> switchMap), which meant the Saving…/Saved! states never rendered — and delete the '@todo investigate' cast that bug hid - suspense: the two comparison panels stack vertically so they fit the ~34%-wide preview pane without a horizontal scrollbar - Drop styled-components from todo-app/form-data in favor of semantic HTML styled by the shared Pico base; remove styled-components and bezier-easing from the website package and the styledComponents compiler flag from next.config.ts Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
- Register the previously-orphaned pages (search, fetch, form-data, event-handlers) in the sidebar and group everything with separators: Basics / Real-world / React integration - Every example page now explains what it demonstrates, which hooks it uses and why, and what to try — in the style of the suspense page - Update the suspense page copy for the stacked panel layout Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Per review: teaching content belongs in tinkerable sandboxes, not
static guide snippets — and nobody uses react-rx to render a top-level
of() as-is.
- /examples/simple now mirrors the RxJS overview (rxjs.dev/guide/overview)
inside React components: counting clicks with a hand-wired
React+RxJS bridge vs react-rx (scan), controlling flow (throttleTime),
and transforming values (map + scan over pointer positions)
- New /examples/basic-state mirrors the useState reference examples:
text field (string), checkbox (boolean, module BehaviorSubject), and
form (two variables) — teaching the useSyncObservable-for-inputs split
- New /examples/timers: Dan Abramov's canonical useInterval hook
contrasted with a switchMap+scan interval stream (delay and running
are stream inputs; count survives swaps); a TimeAgo whose stream emits
{value, unit} data (formatting stays in JSX via Intl.RelativeTimeFormat)
and re-renders once a minute after the first minute thanks to
distinctUntilChanged — with visible render counters; and an SSR
simulation (renderToString → delayed hydrateRoot) that hydrates with
zero mismatches because the initial value derives from the serialized
server clock
- Remove superseded examples and pages (hello-world, simple counter,
reactive-state, tick, fizz-buzz, sync, event-handlers, relative-time)
with redirects for the old URLs; slim the guide's mental-model section
and point it at the runnable tour
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…go examples Until a stream's first emission, a function initialValue is invoked on every snapshot read. The time-ago examples returned a fresh parts object per call, so useSyncExternalStore saw a changed snapshot on every check and hit 'Maximum update depth exceeded' (timer(0, ...) emits asynchronously, so the initialValue path stayed active long enough to loop). Memoize the initial parts and pass them as plain values, and document the footgun in the guide's stability section and the skill's referential-stability reference. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Long-lived sandbox previews left the demos showing '20 minutes ago', with the interesting moments (the flip to minutes, the hydration sequence) long past. Both demos can now re-seed themselves: time-ago rebuilds its messages (remounting the labels resets the render counters), and the SSR simulation regenerates its payload and reruns the renderToString -> delayed hydrateRoot cycle. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
shareReplay({refCount: true}) keeps its buffer after the source resets,
so the next subscriber after an idle period receives a stale tick
synchronously. In the SSR demo that stale emission replaced the
deterministic serialized-clock initial value on re-runs, producing a
hydration mismatch. Plain share() has no replay: every fresh subscribe
renders the initialValue until the next real tick, keeping server and
hydration renders byte-identical on every run.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Root-caused with runtime instrumentation: the rerun's effect cleanup called root.unmount() synchronously while React was committing the rerun. React 19 defers such unmounts, and the deferred commit then fired against the shared container the new run had just filled with fresh static HTML — wiping it (the card vanished for the whole static phase) and leaving hydrateRoot to hydrate an empty container, which produced exactly one recoverable hydration error per rerun. The demo now renders each run into its own keyed container and defers the previous root's unmount by one microtask, so it tears down against the old, detached element. Verified in-browser: reruns keep the static label visible for the full phase and report zero mismatches. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Rick Hanlon's Async React talk demo (rickhanlonii/async-react), rebuilt on react-rx + RxJS under the React integration section. The React side keeps the talk's patterns verbatim — transition router, action-prop design components with useTransition/useOptimistic, CSS animation-delay pending indicators so fast networks never flash loading states — while the data layer becomes streams: - lessons$(tab, search) are revalidate-driven cached observables read through useObservablePromise + use(), so revalidation after a mutation updates visible lists in place and can never re-trigger a Suspense fallback - prefetchLessons() is preloadObservablePromise raced against a 1s timeout, matching the talk's login prefetch semantics - returning to a seen tab gets stale-while-revalidate from shareReplay + startWith - the network debugger is streams end-to-end: BehaviorSubject delay knobs and a scan-based request log ViewTransition animations are omitted (experimental-only API; this runs on stable React), noted in the prose. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Per review: with clicks$ and count$ at top level, the useState/useMemo wrappers disappear and the visible difference between the two sandboxes is exactly the point being taught — the useEffect bridge vs one useObservable line. Applied to all four click sandboxes for consistency, with a note in the prose pointing at Basic state for the per-component useState(() => new Subject()) style. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR delivers a coordinated overhaul of the project’s public-facing surfaces (README + docs + runnable examples) and adds a new installable agent skill for reviewing/writing React components that consume RxJS observables with react-rx.
Changes:
- Adds a new
.agents/skills/react-rx-best-practices/skill and wires it into the docs/LLMs surfaces. - Restructures and expands the docs (new Comparison page, updated Guide/Reference, curated Examples navigation) and refreshes many example implementations (including new “real-world” demos).
- Updates the website sandbox system (shared Pico CSS styling) and removes unused styling dependencies (
styled-components,bezier-easing) plus adds redirects for moved example pages.
Reviewed changes
Copilot reviewed 93 out of 94 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/examples/use-interval/index.tsx | Adds Sandpack entry for the use-interval timers example. |
| website/src/examples/use-interval/App.tsx | Implements Dan Abramov’s canonical useInterval hook example. |
| website/src/examples/todo-app/TodoApp.example.tsx | Refactors todo example toward explicit Subject event inputs + derived list stream. |
| website/src/examples/time-ago/timeAgo.ts | Adds shared clock + timeAgoParts$ derivation for “time ago” labels. |
| website/src/examples/time-ago/index.tsx | Points the route to the new time-ago Sandpack example. |
| website/src/examples/time-ago/App.tsx | Adds the “smarter time-ago” demo UI and memoized initial value handling. |
| website/src/examples/time-ago-ssr/timeAgo.ts | Adds SSR version of the shared clock + timeAgoParts$. |
| website/src/examples/time-ago-ssr/Message.tsx | Adds SSR-safe initial value derivation from serialized server time. |
| website/src/examples/time-ago-ssr/index.tsx | Adds Sandpack entry for the SSR simulation example. |
| website/src/examples/time-ago-ssr/App.tsx | Implements SSR → delayed hydration simulation with mismatch counting and reruns. |
| website/src/examples/tick/TickExample.tsx | Removes superseded tick example entry point. |
| website/src/examples/tick/TickerWithSubTick.tsx | Removes superseded subtick demo component. |
| website/src/examples/tick/Ticker.tsx | Removes superseded delayed tick demo component. |
| website/src/examples/text-field/index.tsx | Points the route to the new text-field Sandpack example. |
| website/src/examples/text-field/App.tsx | Adds controlled text input example using useSyncObservable. |
| website/src/examples/sync/Sync.example.tsx | Removes superseded “sync rendering” example implementation. |
| website/src/examples/suspense/SuspenseExample.tsx | Refactors away from useObservableEvent, improves structure and busy semantics for fallbacks. |
| website/src/examples/simple/Counter.example.tsx | Removes superseded simple counter example implementation. |
| website/src/examples/search/SearchExample.tsx | Refactors search example to explicit Subject event input and clarifies cancellation behavior. |
| website/src/examples/resilient-fetch/index.tsx | Adds Sandpack entry for the offline-resilient polling example. |
| website/src/examples/resilient-fetch/App.tsx | Implements polling with pause/resume, backoff retry, and last-good snapshot retention. |
| website/src/examples/resilient-fetch/api.ts | Adds mock flaky price API and simulated connectivity stream. |
| website/src/examples/reactive-state/ReactiveStateExample.tsx | Removes superseded reactive-state example implementation. |
| website/src/examples/manifests.ts | Updates Sandpack manifests: adds new examples, removes old ones and dropped dependencies. |
| website/src/examples/llm-chat/llm.ts | Adds mock streaming LLM token source observable. |
| website/src/examples/llm-chat/index.tsx | Points the route to the new llm-chat Sandpack example. |
| website/src/examples/llm-chat/chat.ts | Adds cached conversation stream per chat id with token folding + replay. |
| website/src/examples/llm-chat/App.tsx | Implements multi-chat UI with useObservablePromise + <Activity> + hover preloading. |
| website/src/examples/interval-observable/index.tsx | Adds Sandpack entry for interval-as-stream example. |
| website/src/examples/interval-observable/App.tsx | Implements interval demo with stream inputs (BehaviorSubjects) and derived count$. |
| website/src/examples/hello-world/HelloWorldExample.tsx | Removes superseded hello-world example implementation. |
| website/src/examples/form/index.tsx | Points the route to the new form Sandpack example. |
| website/src/examples/form/App.tsx | Adds basic multi-field state example demonstrating useSyncObservable vs useObservable. |
| website/src/examples/form-data/FormDataExample.tsx | Refactors to explicit subjects and fixes submit stream flattening (switching to async write). |
| website/src/examples/fizz-buzz/FizzBuzzExample.tsx | Removes superseded fizz-buzz example implementation. |
| website/src/examples/fetch/FetchExample.tsx | Refactors to explicit Subject event input and removes useObservableEvent usage. |
| website/src/examples/event-handlers/EventHandlersExample.tsx | Removes superseded event-handlers example implementation. |
| website/src/examples/clicks-values/index.tsx | Adds Sandpack entry for “transform values” clicks example. |
| website/src/examples/clicks-values/App.tsx | Implements clicks-to-sum example with throttling and value mapping. |
| website/src/examples/clicks-throttle/index.tsx | Adds Sandpack entry for throttled clicks example. |
| website/src/examples/clicks-throttle/App.tsx | Implements throttled click counter stream. |
| website/src/examples/clicks-count/index.tsx | Updates example route to new click counter example. |
| website/src/examples/clicks-count/App.tsx | Implements click counter using useObservable (no manual subscription bridge). |
| website/src/examples/clicks-bridge/index.tsx | Updates example route to manual bridge example. |
| website/src/examples/clicks-bridge/App.tsx | Implements the “hand-rolled useEffect+subscribe bridge” anti-pattern example. |
| website/src/examples/checkbox/index.tsx | Points the route to the new checkbox Sandpack example. |
| website/src/examples/checkbox/App.tsx | Adds module-scoped BehaviorSubject checkbox example with useSyncObservable. |
| website/src/examples/async-react/server.ts | Adds mock backend for Async React demo. |
| website/src/examples/async-react/router.tsx | Adds transition-based router state + context. |
| website/src/examples/async-react/NetworkDebugger.tsx | Adds network debugger UI driven by streams. |
| website/src/examples/async-react/Login.tsx | Adds login screen with action + prefetch behavior. |
| website/src/examples/async-react/index.tsx | Points the route to the new async-react Sandpack example. |
| website/src/examples/async-react/Home.tsx | Adds the main “lessons” UI reading data via useObservablePromise and actions that revalidate. |
| website/src/examples/async-react/design.tsx | Adds demo “design system” components using transitions/optimistic UI via action props. |
| website/src/examples/async-react/demo.css | Adds shimmer/skeleton/network debugger styling for the Async React demo. |
| website/src/examples/async-react/App.tsx | Wires Async React demo screens with router + debugger. |
| website/src/examples/async-react/api.ts | Implements stream-based data layer: request logging, per-key lessons$, revalidation, prefetching. |
| website/src/examples/animation/AnimationExample.tsx | Removes superseded animation example and its styled-components usage. |
| website/src/content/reference.mdx | Expands API reference with “When not to use” guidance and steers away from useObservableEvent. |
| website/src/content/index.md | Rewrites docs landing page to emphasize concurrent React features, examples, production usage, and skill. |
| website/src/content/guide.md | Restructures guide around mental model, hook selection, events-as-subjects, and key patterns. |
| website/src/content/examples/todo-app.mdx | Adds explanatory prose for the todo example. |
| website/src/content/examples/timers.mdx | Adds new “Timers & time ago” page tying together interval + time-ago + SSR demo. |
| website/src/content/examples/sync.mdx | Removes superseded sync example page. |
| website/src/content/examples/suspense.mdx | Updates Suspense/deferred-values page copy to match new examples/layout. |
| website/src/content/examples/simple.mdx | Replaces old “simple/hello world” content with the new “First steps” guided tour. |
| website/src/content/examples/search.mdx | Adds explanatory prose for search example semantics (switchMap cancellation). |
| website/src/content/examples/resilient-fetch.mdx | Adds explanatory prose for offline pause/resume + retry/backoff polling example. |
| website/src/content/examples/llm-chat.mdx | Adds new LLM streaming demo page with explanation of useObservablePromise, <Activity>, preload. |
| website/src/content/examples/form-data.mdx | Adds explanatory prose for form state + submit state stream example. |
| website/src/content/examples/fetch.mdx | Adds explanatory prose for fetch-on-click example and pointers to related demos. |
| website/src/content/examples/event-handlers.mdx | Removes superseded event-handlers example page. |
| website/src/content/examples/errors.mdx | Adds explanation for error semantics (Error Boundary vs error-as-value). |
| website/src/content/examples/counters.mdx | Removes superseded “advanced counters” page. |
| website/src/content/examples/context.mdx | Adds explanatory prose for context example. |
| website/src/content/examples/basic-state.mdx | Adds new “Basic state” page (text field, checkbox, form) aligned with React docs. |
| website/src/content/examples/async-react.mdx | Adds Async React demo page and explanation of stream-based data layer. |
| website/src/content/examples/animation.mdx | Removes superseded animation example page. |
| website/src/content/examples/_meta.ts | Curates examples navigation into Basics / Real-world / React integration sections. |
| website/src/content/comparison.md | Adds new comparison page: other RxJS bindings + state-library landscape framing. |
| website/src/content/_meta.ts | Adds comparison to top-level docs navigation. |
| website/src/components/Sandpack.tsx | Adds shared Pico CSS + narrow-pane styles and updates Sandpack entrypoint wiring. |
| website/src/app/llms.txt/route.ts | Adds agent skill section to llms.txt. |
| website/package.json | Drops styled-components and bezier-easing from website deps. |
| website/next.config.ts | Removes styled-components compiler flag and adds redirects for moved/removed example pages. |
| pnpm-lock.yaml | Updates lockfile to reflect removed deps. |
| packages/react-rx/README.md | Major README refresh: purpose, Suspense section, comparisons, production usage, and skill install. |
| AGENTS.md | Documents skill usage guidance for agents and reviewers. |
| .agents/skills/react-rx-best-practices/SKILL.md | Adds new installable skill definition + best practices content. |
| .agents/skills/react-rx-best-practices/references/when-subscribe-is-right.md | Adds reference doc covering when manual subscriptions should remain. |
| .agents/skills/react-rx-best-practices/references/stream-state-recipes.md | Adds reference doc with recurring refactor/stream-state recipes. |
| .agents/skills/react-rx-best-practices/references/referential-stability.md | Adds reference doc on observable identity and stable construction patterns. |
| .agents/skills/react-rx-best-practices/references/hook-selection.md | Adds decision guide for hook selection and “when not to use” lists. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| parts$ = now$.pipe( | ||
| map((now) => toTimeAgoParts(now - sentAt)), | ||
| distinctUntilChanged( | ||
| (a, b) => | ||
| a.value === b.value && | ||
| a.unit === b.unit, | ||
| ), | ||
| ) |
| parts$ = now$.pipe( | ||
| map((now) => toTimeAgoParts(now - sentAt)), | ||
| distinctUntilChanged( | ||
| (a, b) => | ||
| a.value === b.value && | ||
| a.unit === b.unit, | ||
| ), | ||
| ) |
| map(([, text]) => text.trim()), | ||
| filter((text) => text.length > 0), | ||
| map((text) => ({text, id: Date.now()})), | ||
| scan( | ||
| (items: TodoItem[], item) => |
A copy pass over the README and the docs site (skills untouched): - No em dashes anywhere: prose, code comments, and UI strings in the example sandboxes - Shorter sentences, one idea each, with bold lead-ins on bullets - Long paragraphs broken into lists (guide semantics/options sections, example pages, comparison prose) - Jargon removed: 'load-bearing' is now plain language, 'chrome' is 'UI', symbol-only table cells spell out No/Manual instead of a dash - Table cells and links unchanged in meaning; all heading anchors kept Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 97 out of 98 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
website/src/examples/resilient-fetch/App.tsx:120
- The click handler toggles
online$based ononlineread viauseObservable. SinceuseObservablecan lag (deferred updates), this is a same-event read-back and may toggle from a stale value under concurrent rendering. Read from the BehaviorSubject directly (or switch this read touseSyncObservable).
website/src/examples/async-react/design.tsx:70 CompleteButtonstays clickable while its transition is pending. That allows multiple rapid clicks to queue concurrentaction()calls and flip optimistic state based on a potentially stale closure, producing inconsistent final state (and duplicate requests). Disable the button (and mark it busy) while pending, likeButtondoes.
…to react-rx The subtree merge in the previous commit brings in github.com/rickhanlonii/async-react with full history. This commit turns it into a pnpm workspace and performs the migration: - src/data/index.js: the cache-of-promises becomes cached revalidate-driven rxjs streams. getLessons$(tab, search) is read as a use()-compatible promise, so revalidations stream fresh data into visible lists in place and can never re-trigger a Suspense fallback. prefetchLessons() is preloadObservablePromise raced against 1s, preserving the talk's login semantics. refreshLessons() resolves when fresh data lands so actions stay pending through mutation + refetch - src/app/Home.jsx: reads the stream via useObservablePromise in the non-suspending parent and passes the promise to LessonList's use(); completeAction awaits refreshLessons instead of router.refresh - Everything else stays upstream: router, design components, ViewTransition animations, network debugger, styling - Workspace plumbing: pnpm-workspace entry, react-rx/rxjs deps (react-scripts removed, unused CRA leftover), vite resolve.dedupe for react so workspace-source react-rx uses the app's experimental React, excluded from oxlint/oxfmt/knip to stay close to upstream, yarn.lock dropped in favor of the monorepo lockfile Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Sandpack's in-browser bundler can't run the fork: its package manager hard-fails Radix/react-aria peer ranges against react@experimental (0.0.0-experimental versions), and the Vite Tailwind plugin doesn't exist there either. Instead of stripping ViewTransition and faking the styling, the docs embed the real thing: - website/scripts/build-async-react-demo.mjs builds the workspace app with a /async-react-demo/ base and copies dist into public/ (wired into the website's predev/prebuild; output gitignored) - the example page renders an iframe with a Lessons / Login flow switcher and links to the workspace sources; an SPA rewrite serves index.html for client-side routes like /async-react-demo/login - the fork's router learns to strip Vite's base from the initial and navigated pathnames, so the app works from a subpath (no behavior change when served from /) - the example manifest keeps reading the workspace sources (new sourceDir field on ExampleManifest), so Copy page and /llms-full.txt still export the real migrated files - the superseded hand-written sandbox port of the demo is removed Verified in the browser: the embed boots with upstream styling intact, ViewTransition animations play (list items animate out on completion), the network debugger works, and the login flow runs through the switcher. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5e7d838. Configure here.
|
|
||
| function navigate(url) { | ||
| window.navigation.navigate(url); | ||
| } |
There was a problem hiding this comment.
Login navigation drops Vite base
High Severity
normalizePathname strips import.meta.env.BASE_URL for route matching, but navigate (and History setParams) write bare paths like /. When the docs embed builds with --base=/async-react-demo/, login then leaves the SPA subpath—the iframe loads the site root under the Navigation API, and History mode rewrites the address bar out of the demo.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 5e7d838. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 147 out of 150 changed files in this pull request and generated no new comments.
Suppressed comments (7)
async-react/src/main.jsx:48
Layoutis passed aheadingprop, butLayoutonly accepts{children}and never rendersheading. This is dead code and can confuse future edits (it looks like the heading should show up, but it can’t).
async-react/src/design/TabList.jsx:3- Import path contains a double slash (
@/components//ui/tabs). While it may resolve in some environments, it’s easy to break tooling (and looks like a typo).
async-react/src/design/PendingButton.jsx:1 - Import path contains a double slash (
@/components//ui/button). This looks accidental and can break tools that don’t normalize paths the same way.
async-react/server.js:6 server.jsimports@/data/fake-data.js, but Node (vianodemon server.js) won't understand the Vite@alias. This makes the optional backend server script fail to start with a module resolution error.
const port = 8080;
import * as fakeData from "@/data/fake-data.js";
async-react/src/design/EmptyList.jsx:11
- Import path contains a double slash (
@/components//ui/item). This looks accidental and can break tools that don’t normalize paths the same way.
async-react/src/design/Fallback.jsx:7 - Import path contains a double slash (
@/components//ui/item). This looks accidental and can break tools that don’t normalize paths the same way.
async-react/src/data/index.js:49 refreshLessons()waits forsettled$to emit, butsettled$is only notified on successful fetches. If/lessonserrors (real server mode, network failure, etc),refreshLessons()will never resolve and any Action awaiting it will stay pending indefinitely.


Description
A coordinated glow-up of the project's three public surfaces, plus a new installable agent skill and a forked showcase app.
Closes #38
Closes #56
Agent skill (
.agents/skills/react-rx-best-practices/)A best-practices skill (explicitly not a version-migration skill) for writing and reviewing React components that consume observables. Organized around idioms and anti-patterns: the hand-rolled
useState+useEffect+.subscribe()bridge, hook selection with per-hook "when not to use" lists, referential stability (including React Compiler detection and the lazy-initialValuefootgun), deriving state in the stream, preferring explicitSubjects overuseObservableEvent, and the cases where a manual subscription is the right design. Recipes distilled from sanity-io/sanity#13788/#13799/#13814/#13828 and sanity-io/plugins#1820.Authoritative source in
.agents/skills/with a.claude/skillssymlink, installable vianpx skills add sanity-io/react-rx --skill react-rx-best-practices. Advertised in the README, the docs landing page, and a dedicatedllms.txtsection.The async-react fork (
async-react/workspace)The final demo from Rick Hanlon's React Conf 2025 Async React talk, forked from rickhanlonii/async-react via
git subtree(full history preserved) and migrated to react-rx + RxJS in place:src/data/index.js: the cache-of-promises becomes cached revalidate-driven streams.getLessons$(tab, search)is read throughuseObservablePromise+use(), so revalidations stream fresh data into visible lists in place and can never re-trigger a Suspense fallback.prefetchLessons()ispreloadObservablePromiseraced against 1s, preserving the talk's login semantics.refreshLessons()resolves when fresh data lands so action pending states cover mutation + refetch.src/app/Home.jsxreads the stream in the non-suspending parent and passes the promise toLessonList'suse().<ViewTransition>animations, network debugger, styling. Excluded from oxlint/oxfmt/knip to stay close to upstream.The docs page embeds the real Vite build of this workspace (built in the website's predev/prebuild, served from
public/, with an SPA rewrite for/async-react-demo/loginand a Lessons / Login flow switcher). Sandpack could not run it: its package manager hard-fails Radix/react-aria peer ranges againstreact@experimental, and the Vite Tailwind plugin doesn't exist in-browser. The page's "Copy page" /llms-full.txtstill export the actual workspace sources via the example manifest's newsourceDirsupport.README
Leads with what react-rx is for (live streams, time, resilient fetching), a before/after of the hand-rolled bridge, concurrent-React features (identity-coherent deferral,
<Activity>, SSR, React Compiler-tested suite), a named Suspense section (answers #56), real-world use cases, an honest comparison table vsobservable-hooks/@react-rxjs/core/ DIY, a Used in production section (answers #38: Sanity Studio + adoption PRs, the plugin ecosystem, navikt/aksel), and the skill install.Docs
useObservableEventdocuments the preferred explicit-Subjectalternative (no@deprecatedyet).Examples
useStatereference examples; Timers & time ago covers Dan Abramov'suseIntervalvs aswitchMap+scanstream, adistinctUntilChangedTimeAgo with visible render counters, and an SSR simulation hydrating with zero mismatches. Superseded examples removed with redirects.llm-chat(concurrent conversations behind<Activity mode="hidden">, hover preload, in-place token streaming),resilient-fetch(retry/backoff, offline pause/resume, keep last-good data).todo-app,search,form-data,suspense,fetchonto explicitSubjecthandlers; fixed a latentform-databug (map→switchMap, the "Saving…/Saved!" states never rendered).externalResources+ narrow-pane tuning; no horizontal scrollbars at the 66/34 layout;styled-componentsandbezier-easingdropped.Bugs found and fixed along the way
initialValueloop: a functioninitialValuereturning a fresh object per call loopsuseSyncExternalStorebefore the first async emission. Fixed by memoizing; documented in the guide and the skill.root.unmount()inside an effect cleanup is deferred by React 19 mid-commit, and the deferred commit wiped the container the new run had just filled. Fixed with a per-run keyed container + microtask-deferred unmount.shareReplay({refCount: true})on a shared clock replaced with plainshare()to avoid stale-tick replay.What to review
async-react/src/data/index.js,src/app/Home.jsx, router base normalization) against upstreamuseObservableEventsteer-away tone (docs-level only, no source deprecation)Testing
pnpm lint,pnpm knip,pnpm test(278 passed, 4 expected-fail by design),pnpm buildall green<ViewTransition>animations play, optimistic toggle + in-place revalidation, login flow with prefetch race, network debugger sliders, zero console errors/llms.txt+/llms-full.txtinclude the skill section and the fork's migrated sources; old example URLs 307-redirectDemos
LLM chat: token streaming, hover preload skipping the fallback, and hidden-
<Activity>chats revealing fully caught up:llm_chat_streaming_activity_preload_demo.mp4
Offline-resilient fetch: polling pauses offline and resumes immediately on reconnect:
resilient_fetch_offline_pause_resume_demo.mp4
Time ago:
distinctUntilChangedrender counters, minute-old labels go quiet:time_ago_distinct_until_changed_render_counts_demo.mp4
New comparison page with both tables
form-data example showing the Saved! state that the fixed switchMap now reaches
To show artifacts inline, enable in settings.