Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/compat-use.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/react": minor
---

Export `use` from `@lynx-js/react/compat`.
15 changes: 12 additions & 3 deletions packages/react/runtime/__test__/snapshot/compat/export.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, it, expect, vi } from 'vitest';
import ReactLynx from '@lynx-js/react';
import { startTransition as preactStartTransition, useTransition as preactUseTransition } from 'preact/compat';
import {
startTransition as preactStartTransition,
use as preactUse,
useTransition as preactUseTransition,
} from 'preact/compat';

import compat from '../../../compat';

Expand All @@ -12,6 +16,11 @@ describe('Default export', () => {
});
});

it('should include use', () => {
expect(compat).toHaveProperty('use');
expect(compat.use).toBe(preactUse);
});

it('should include startTransition and useTransition', () => {
expect(compat).toHaveProperty('startTransition');
expect(compat.startTransition).toBe(preactStartTransition);
Expand All @@ -21,8 +30,8 @@ describe('Default export', () => {
});

it('should have correct number of exports', () => {
// +2 for startTransition and useTransition
const expectedExportCount = Object.keys(ReactLynx).length + 2;
// +3 for startTransition, use and useTransition
const expectedExportCount = Object.keys(ReactLynx).length + 3;
expect(Object.keys(compat).length).toBe(expectedExportCount);
});
});
111 changes: 111 additions & 0 deletions packages/react/runtime/__test__/snapshot/compat/use.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2026 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import { render } from 'preact';
import { Suspense } from 'preact/compat';
import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';

import { createContext } from '../../../src/index';
import { use } from '../../../compat';
import { setupBackgroundDocument } from '../../../src/document';
import { backgroundSnapshotInstanceManager, setupPage } from '../../../src/snapshot';
import { globalEnvManager } from '../utils/envManager';
import { elementTree, waitSchedule } from '../utils/nativeMethod';

describe('use', () => {
/** @type {import('../../../src/snapshot').SnapshotInstance} */
let scratch;

beforeAll(() => {
setupBackgroundDocument();
setupPage(__CreatePage('0', 0));
});

beforeEach(() => {
globalEnvManager.switchToBackground();
scratch = document.createElement('root');
});

afterEach(() => {
render(null, scratch);
elementTree.clear();
backgroundSnapshotInstanceManager.clear();
});

it('reads a context value', () => {
const Theme = createContext('light');
let seen;

function Reader() {
seen = use(Theme);
return <view />;
}

render(
<Theme.Provider value='dark'>
<Reader />
</Theme.Provider>,
scratch,
);

expect(seen).toBe('dark');
});

it('falls back to the default context value outside a provider', () => {
const Theme = createContext('light');
let seen;

function Reader() {
seen = use(Theme);
return <view />;
}

render(<Reader />, scratch);

expect(seen).toBe('light');
});

it('may be called conditionally', () => {
const Theme = createContext('light');
const seen = [];

function Reader({ enabled }) {
seen.push(enabled ? use(Theme) : 'skipped');
return <view />;
}

render(<Reader enabled={false} />, scratch);
render(<Reader enabled />, scratch);

expect(seen).toEqual(['skipped', 'light']);
});

it('suspends on a pending promise and renders the resolved value', async () => {
let resolvePromise;
const promise = new Promise(resolve => {
resolvePromise = resolve;
});
let seen;

function Reader() {
seen = use(promise);
return <view />;
}

render(
<Suspense fallback={<text>loading</text>}>
<Reader />
</Suspense>,
scratch,
);

// Still pending: `use` threw the promise, so the boundary shows its fallback.
expect(seen).toBeUndefined();

resolvePromise('done');
await waitSchedule();

expect(seen).toBe('done');
});
});
23 changes: 22 additions & 1 deletion packages/react/runtime/compat/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2025 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import type { Context } from 'preact';

export * from '@lynx-js/react';

/**
Expand Down Expand Up @@ -30,11 +32,30 @@ declare function startTransition(cb: () => void): void;
* @public
*/
declare function useTransition(): [false, typeof startTransition];
export { startTransition, useTransition };

/**
* Reads the value of a resource during render.
*
* Unlike the other hooks, `use` may be called conditionally and inside loops.
*
* - Given a promise, it returns the resolved value. While the promise is
* pending it suspends, so the nearest `Suspense` boundary renders its
* fallback; a rejected promise throws.
* - Given a context, it returns the current value, subscribing the component
* to that context.
*
* @param resource - A promise or a context to read
* @returns The resolved value of the promise, or the current context value
*
* @public
*/
declare function use<T>(resource: Promise<T> | Context<T>): T;
export { startTransition, use, useTransition };

// type for the default export
declare const _default: typeof import('@lynx-js/react') & {
startTransition: typeof startTransition;
use: typeof use;
useTransition: typeof useTransition;
};
export default _default;
5 changes: 3 additions & 2 deletions packages/react/runtime/compat/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Copyright 2025 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { startTransition, useTransition } from 'preact/compat';
import { startTransition, use, useTransition } from 'preact/compat';

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

export default /*#__PURE__*/ Object.assign({}, ReactLynx, {
startTransition,
use,
useTransition,
});

export * from '@lynx-js/react';
export { startTransition, useTransition };
export { startTransition, use, useTransition };
1 change: 1 addition & 0 deletions packages/react/runtime/lazy/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const {

// compat
startTransition,
use,
useTransition,
} = target[sExportsReactCompat];

Expand Down
Loading