Skip to content

Commit a329f3b

Browse files
committed
feat(react): export use from @lynx-js/react/compat
Preact ships `use` in compat but ReactLynx did not forward it, so cards could not read a promise or a context during render. Covers both resources: a context read (including the default value and a conditional call) and a promise that suspends to a Suspense boundary and then renders its resolved value.
1 parent 2a20a67 commit a329f3b

6 files changed

Lines changed: 154 additions & 6 deletions

File tree

.changeset/compat-use.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lynx-js/react": minor
3+
---
4+
5+
Export `use` from `@lynx-js/react/compat`.

packages/react/runtime/__test__/snapshot/compat/export.test.jsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, it, expect, vi } from 'vitest';
22
import ReactLynx from '@lynx-js/react';
3-
import { startTransition as preactStartTransition, useTransition as preactUseTransition } from 'preact/compat';
3+
import {
4+
startTransition as preactStartTransition,
5+
use as preactUse,
6+
useTransition as preactUseTransition,
7+
} from 'preact/compat';
48

59
import compat from '../../../compat';
610

@@ -12,6 +16,11 @@ describe('Default export', () => {
1216
});
1317
});
1418

19+
it('should include use', () => {
20+
expect(compat).toHaveProperty('use');
21+
expect(compat.use).toBe(preactUse);
22+
});
23+
1524
it('should include startTransition and useTransition', () => {
1625
expect(compat).toHaveProperty('startTransition');
1726
expect(compat.startTransition).toBe(preactStartTransition);
@@ -21,8 +30,8 @@ describe('Default export', () => {
2130
});
2231

2332
it('should have correct number of exports', () => {
24-
// +2 for startTransition and useTransition
25-
const expectedExportCount = Object.keys(ReactLynx).length + 2;
33+
// +3 for startTransition, use and useTransition
34+
const expectedExportCount = Object.keys(ReactLynx).length + 3;
2635
expect(Object.keys(compat).length).toBe(expectedExportCount);
2736
});
2837
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2026 The Lynx Authors. All rights reserved.
2+
// Licensed under the Apache License Version 2.0 that can be found in the
3+
// LICENSE file in the root directory of this source tree.
4+
5+
import { render } from 'preact';
6+
import { Suspense } from 'preact/compat';
7+
import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
8+
9+
import { createContext } from '../../../src/index';
10+
import { use } from '../../../compat';
11+
import { setupBackgroundDocument } from '../../../src/document';
12+
import { backgroundSnapshotInstanceManager, setupPage } from '../../../src/snapshot';
13+
import { globalEnvManager } from '../utils/envManager';
14+
import { elementTree, waitSchedule } from '../utils/nativeMethod';
15+
16+
describe('use', () => {
17+
/** @type {import('../../../src/snapshot').SnapshotInstance} */
18+
let scratch;
19+
20+
beforeAll(() => {
21+
setupBackgroundDocument();
22+
setupPage(__CreatePage('0', 0));
23+
});
24+
25+
beforeEach(() => {
26+
globalEnvManager.switchToBackground();
27+
scratch = document.createElement('root');
28+
});
29+
30+
afterEach(() => {
31+
render(null, scratch);
32+
elementTree.clear();
33+
backgroundSnapshotInstanceManager.clear();
34+
});
35+
36+
it('reads a context value', () => {
37+
const Theme = createContext('light');
38+
let seen;
39+
40+
function Reader() {
41+
seen = use(Theme);
42+
return <view />;
43+
}
44+
45+
render(
46+
<Theme.Provider value='dark'>
47+
<Reader />
48+
</Theme.Provider>,
49+
scratch,
50+
);
51+
52+
expect(seen).toBe('dark');
53+
});
54+
55+
it('falls back to the default context value outside a provider', () => {
56+
const Theme = createContext('light');
57+
let seen;
58+
59+
function Reader() {
60+
seen = use(Theme);
61+
return <view />;
62+
}
63+
64+
render(<Reader />, scratch);
65+
66+
expect(seen).toBe('light');
67+
});
68+
69+
it('may be called conditionally', () => {
70+
const Theme = createContext('light');
71+
const seen = [];
72+
73+
function Reader({ enabled }) {
74+
seen.push(enabled ? use(Theme) : 'skipped');
75+
return <view />;
76+
}
77+
78+
render(<Reader enabled={false} />, scratch);
79+
render(<Reader enabled />, scratch);
80+
81+
expect(seen).toEqual(['skipped', 'light']);
82+
});
83+
84+
it('suspends on a pending promise and renders the resolved value', async () => {
85+
let resolvePromise;
86+
const promise = new Promise(resolve => {
87+
resolvePromise = resolve;
88+
});
89+
let seen;
90+
91+
function Reader() {
92+
seen = use(promise);
93+
return <view />;
94+
}
95+
96+
render(
97+
<Suspense fallback={<text>loading</text>}>
98+
<Reader />
99+
</Suspense>,
100+
scratch,
101+
);
102+
103+
// Still pending: `use` threw the promise, so the boundary shows its fallback.
104+
expect(seen).toBeUndefined();
105+
106+
resolvePromise('done');
107+
await waitSchedule();
108+
109+
expect(seen).toBe('done');
110+
});
111+
});

packages/react/runtime/compat/index.d.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2025 The Lynx Authors. All rights reserved.
22
// Licensed under the Apache License Version 2.0 that can be found in the
33
// LICENSE file in the root directory of this source tree.
4+
import type { Context } from 'preact';
5+
46
export * from '@lynx-js/react';
57

68
/**
@@ -30,11 +32,30 @@ declare function startTransition(cb: () => void): void;
3032
* @public
3133
*/
3234
declare function useTransition(): [false, typeof startTransition];
33-
export { startTransition, useTransition };
35+
36+
/**
37+
* Reads the value of a resource during render.
38+
*
39+
* Unlike the other hooks, `use` may be called conditionally and inside loops.
40+
*
41+
* - Given a promise, it returns the resolved value. While the promise is
42+
* pending it suspends, so the nearest `Suspense` boundary renders its
43+
* fallback; a rejected promise throws.
44+
* - Given a context, it returns the current value, subscribing the component
45+
* to that context.
46+
*
47+
* @param resource - A promise or a context to read
48+
* @returns The resolved value of the promise, or the current context value
49+
*
50+
* @public
51+
*/
52+
declare function use<T>(resource: Promise<T> | Context<T>): T;
53+
export { startTransition, use, useTransition };
3454

3555
// type for the default export
3656
declare const _default: typeof import('@lynx-js/react') & {
3757
startTransition: typeof startTransition;
58+
use: typeof use;
3859
useTransition: typeof useTransition;
3960
};
4061
export default _default;
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
// Copyright 2025 The Lynx Authors. All rights reserved.
22
// Licensed under the Apache License Version 2.0 that can be found in the
33
// LICENSE file in the root directory of this source tree.
4-
import { startTransition, useTransition } from 'preact/compat';
4+
import { startTransition, use, useTransition } from 'preact/compat';
55

66
/* eslint-disable-next-line import/default */
77
import ReactLynx from '@lynx-js/react';
88

99
export default /*#__PURE__*/ Object.assign({}, ReactLynx, {
1010
startTransition,
11+
use,
1112
useTransition,
1213
});
1314

1415
export * from '@lynx-js/react';
15-
export { startTransition, useTransition };
16+
export { startTransition, use, useTransition };

packages/react/runtime/lazy/compat.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const {
5151

5252
// compat
5353
startTransition,
54+
use,
5455
useTransition,
5556
} = target[sExportsReactCompat];
5657

0 commit comments

Comments
 (0)