-
Notifications
You must be signed in to change notification settings - Fork 17
fix: support React 16+ in @gram-ai/elements #1422
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
Open
adaam2
wants to merge
14
commits into
main
Choose a base branch
from
fix/elements-older-react-versions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+331
−8
Open
Changes from 6 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
7ac2309
fix: support React 16+ in @gram-ai/elements
adaam2 c178a3d
fix: add React shim and test app Vite config for React 16/17
adaam2 6e2e247
fix: embed session server in Vite dev server plugin
adaam2 65529b9
fix: simplify session handling and add MCP config to test apps
adaam2 898d555
fix: remove test apps from workspace to fix type conflicts
adaam2 a7384ef
fix: remove test-apps from elements package
adaam2 c1765e2
feat: ship reactCompat() Vite plugin for React 16/17 support
adaam2 0c4d591
refactor: deduplicate React polyfill logic into shared compat-shims
adaam2 1d0b6ad
fix: address Devin review — try-catch getSnapshot, add missing polyfills
adaam2 8394be8
refactor: compact compat files — trim verbose comments and use loop p…
adaam2 239e25d
fix: resolve merge conflict with main — keep both Replay and React Co…
adaam2 5ba18a9
chore: regenerate goa code (v3.24.1 → v3.24.2)
adaam2 9a00a29
revert: restore server/gen to main — undo accidental goa version bump
adaam2 3ffed70
chore: add changeset for React compat plugin
adaam2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import * as React from 'react' | ||
|
|
||
| /** | ||
| * Tests for the React compatibility shims in compat.ts. | ||
| * | ||
| * We can't simulate missing React APIs by deleting properties from the ES | ||
| * module namespace (it's frozen). Instead we verify: | ||
| * 1. The compat module doesn't break existing React 19 APIs | ||
| * 2. The polyfill implementations work correctly in isolation | ||
| */ | ||
|
|
||
| // Import compat to ensure it runs without errors on React 19 | ||
| import './compat' | ||
|
|
||
| describe('compat', () => { | ||
| describe('existing React 19 APIs are preserved', () => { | ||
| it('React.useSyncExternalStore exists and is the original', () => { | ||
| expect(typeof React.useSyncExternalStore).toBe('function') | ||
| }) | ||
|
|
||
| it('React.useId exists and is the original', () => { | ||
| expect(typeof React.useId).toBe('function') | ||
| }) | ||
|
|
||
| it('React.useInsertionEffect exists and is the original', () => { | ||
| expect(typeof React.useInsertionEffect).toBe('function') | ||
| }) | ||
| }) | ||
|
|
||
| describe('useSyncExternalStore polyfill implementation', () => { | ||
| // Test the polyfill logic in isolation by extracting the same algorithm | ||
| it('returns the current snapshot value', () => { | ||
| let value = 'initial' | ||
| const getSnapshot = () => value | ||
| const subscribe = (cb: () => void) => { | ||
| // Simulate a subscription | ||
| void cb | ||
| return () => {} | ||
| } | ||
|
|
||
| // The real polyfill is a React hook and can't be called outside a | ||
| // component, but we can verify the algorithm: it calls getSnapshot() | ||
| // to get the current value. | ||
| const result = getSnapshot() | ||
| expect(result).toBe('initial') | ||
|
|
||
| value = 'updated' | ||
| expect(getSnapshot()).toBe('updated') | ||
| void subscribe | ||
| }) | ||
| }) | ||
|
|
||
| describe('useId polyfill implementation', () => { | ||
| it('generates unique IDs with the expected format', () => { | ||
| // Simulate the counter-based ID generation used by the polyfill | ||
| let counter = 0 | ||
| const generateId = () => `:r${counter++}:` | ||
|
|
||
| const id1 = generateId() | ||
| const id2 = generateId() | ||
| const id3 = generateId() | ||
|
|
||
| expect(id1).toMatch(/^:r\d+:$/) | ||
| expect(id2).toMatch(/^:r\d+:$/) | ||
| expect(id3).toMatch(/^:r\d+:$/) | ||
|
|
||
| // All IDs must be unique | ||
| expect(new Set([id1, id2, id3]).size).toBe(3) | ||
| }) | ||
| }) | ||
|
|
||
| describe('useInsertionEffect polyfill', () => { | ||
| it('falls back to useLayoutEffect which exists on all React versions', () => { | ||
| // The polyfill assigns useLayoutEffect as the fallback. | ||
| // Verify useLayoutEffect exists (available since React 16.8). | ||
| expect(typeof React.useLayoutEffect).toBe('function') | ||
| }) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * React compatibility shims for React 16.8+ | ||
| * | ||
| * This module polyfills React 18 APIs that are used by transitive dependencies | ||
| * (zustand, @assistant-ui/react, @tanstack/react-query) so that elements can | ||
| * run on older React versions. | ||
| * | ||
| * Must be imported before any other modules that depend on these APIs. | ||
| * | ||
| * Based on: https://www.assistant-ui.com/docs/react-compatibility | ||
| */ | ||
|
|
||
| import * as React from 'react' | ||
|
|
||
| // Cast to mutable record for patching | ||
| const ReactMutable = React as Record<string, unknown> | ||
|
|
||
| /** | ||
| * Polyfill useSyncExternalStore (React 18+) | ||
| * | ||
| * Used by zustand and @tanstack/react-query. This is a simplified shim based | ||
| * on the official `use-sync-external-store/shim` package from the React team. | ||
| * It uses useState + useEffect to subscribe, which is safe for React 16.8+. | ||
| */ | ||
| if (typeof ReactMutable.useSyncExternalStore !== 'function') { | ||
| ReactMutable.useSyncExternalStore = function useSyncExternalStore<T>( | ||
| subscribe: (onStoreChange: () => void) => () => void, | ||
| getSnapshot: () => T, | ||
| getServerSnapshot?: () => T | ||
| ): T { | ||
| // Server snapshot is only relevant for SSR with React 18's streaming renderer. | ||
| // For older React, we always use getSnapshot. | ||
| void getServerSnapshot | ||
|
|
||
| const value = getSnapshot() | ||
| const [{ inst }, forceUpdate] = React.useState({ | ||
| inst: { value, getSnapshot }, | ||
| }) | ||
|
|
||
| React.useLayoutEffect(() => { | ||
| inst.value = value | ||
| inst.getSnapshot = getSnapshot | ||
|
|
||
| if (!Object.is(inst.value, inst.getSnapshot())) { | ||
| forceUpdate({ inst }) | ||
| } | ||
| }, [subscribe, value, getSnapshot]) | ||
|
|
||
| React.useEffect(() => { | ||
| if (!Object.is(inst.value, inst.getSnapshot())) { | ||
| forceUpdate({ inst }) | ||
| } | ||
|
|
||
| return subscribe(() => { | ||
| if (!Object.is(inst.value, inst.getSnapshot())) { | ||
| forceUpdate({ inst }) | ||
| } | ||
| }) | ||
| }, [subscribe]) | ||
|
|
||
| return value | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Polyfill useId (React 18+) | ||
| * | ||
| * Used by @assistant-ui/react and Radix UI primitives. Generates a stable ID | ||
| * per component instance using useRef, matching React 18 semantics. | ||
| */ | ||
| if (typeof ReactMutable.useId !== 'function') { | ||
| let counter = 0 | ||
| ReactMutable.useId = function useId(): string { | ||
| const ref = React.useRef<string | null>(null) | ||
| if (ref.current === null) { | ||
| ref.current = `:r${counter++}:` | ||
| } | ||
| return ref.current | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Polyfill useInsertionEffect (React 18+) | ||
| * | ||
| * Used by CSS-in-JS libraries. Falls back to useLayoutEffect which has the | ||
| * same synchronous timing guarantees. | ||
| */ | ||
| if (typeof ReactMutable.useInsertionEffect !== 'function') { | ||
| ReactMutable.useInsertionEffect = React.useLayoutEffect | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.