Skip to content

Commit 0135d60

Browse files
Type Signal as valid React JSX (#271)
* type signal as a react element * silence typescript errors * refactor react adapter typing * add computed text binding test * add changeset --------- Co-authored-by: Jovi De Croock <[email protected]>
1 parent ad5a485 commit 0135d60

File tree

5 files changed

+36
-9
lines changed

5 files changed

+36
-9
lines changed

.changeset/metal-emus-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals-react": patch
3+
---
4+
5+
type Signal as a React Element

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ function addDependency(signal: Signal): Node | undefined {
183183
return undefined;
184184
}
185185

186+
// @ts-ignore internal Signal is viewed as a function
186187
declare class Signal<T = any> {
187188
/** @internal */
188189
_value: unknown;
@@ -224,6 +225,7 @@ declare class Signal<T = any> {
224225
}
225226

226227
/** @internal */
228+
// @ts-ignore internal Signal is viewed as function
227229
function Signal(this: Signal, value?: unknown) {
228230
this._value = value;
229231
this._version = 0;

packages/react/src/index.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
useEffect,
55
Component,
66
type FunctionComponent,
7+
type ReactElement,
78
} from "react";
89
import React from "react";
910
import jsxRuntime from "react/jsx-runtime";
@@ -44,7 +45,11 @@ const ProxyHandlers = {
4445
*
4546
* @see https://github.com/facebook/react/blob/2d80a0cd690bb5650b6c8a6c079a87b5dc42bd15/packages/react-reconciler/src/ReactFiberHooks.old.js#L460
4647
*/
47-
apply(Component: FunctionComponent, thisArg: any, argumentsList: any) {
48+
apply(
49+
Component: FunctionComponent<any>,
50+
thisArg: any,
51+
argumentsList: Parameters<FunctionComponent<any>>
52+
) {
4853
const store = useMemo(createEffectStore, Empty);
4954

5055
useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
@@ -54,6 +59,7 @@ const ProxyHandlers = {
5459
try {
5560
const children = Component.apply(thisArg, argumentsList);
5661
return children;
62+
// eslint-disable-next-line no-useless-catch
5763
} catch (e) {
5864
// Re-throwing promises that'll be handled by suspense
5965
// or an actual error.
@@ -69,6 +75,7 @@ const ProxyHandlers = {
6975
function ProxyFunctionalComponent(Component: FunctionComponent<any>) {
7076
return ProxyInstance.get(Component) || WrapWithProxy(Component);
7177
}
78+
7279
function WrapWithProxy(Component: FunctionComponent<any>) {
7380
if (SupportsProxy) {
7481
const ProxyComponent = new Proxy(Component, ProxyHandlers);
@@ -93,9 +100,10 @@ function WrapWithProxy(Component: FunctionComponent<any>) {
93100
* el.type.defaultProps;
94101
* ```
95102
*/
96-
const WrappedComponent = function () {
97-
return ProxyHandlers.apply(Component, undefined, arguments);
103+
const WrappedComponent: FunctionComponent<any> = (...args) => {
104+
return ProxyHandlers.apply(Component, undefined, args);
98105
};
106+
99107
ProxyInstance.set(Component, WrappedComponent);
100108
ProxyInstance.set(WrappedComponent, WrappedComponent);
101109

@@ -204,6 +212,12 @@ JsxPro.jsxs && /* */ (JsxPro.jsxs = WrapJsx(JsxPro.jsxs));
204212
JsxDev.jsxDEV && /**/ (JsxDev.jsxDEV = WrapJsx(JsxDev.jsxDEV));
205213
JsxPro.jsxDEV && /**/ (JsxPro.jsxDEV = WrapJsx(JsxPro.jsxDEV));
206214

215+
declare module "@preact/signals-core" {
216+
// @ts-ignore internal Signal is viewed as function
217+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
218+
interface Signal extends ReactElement {}
219+
}
220+
207221
/**
208222
* A wrapper component that renders a Signal's value directly as a Text node.
209223
*/

packages/react/src/internal.d.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import { Signal } from "@preact/signals-core";
2-
31
export interface Effect {
42
_sources: object | undefined;
53
_start(): () => void;
64
_callback(): void;
75
_dispose(): void;
86
}
97

10-
export type Updater = Signal<unknown>;
11-
128
export interface JsxRuntimeModule {
139
jsx?(type: any, ...rest: any[]): unknown;
1410
jsxs?(type: any, ...rest: any[]): unknown;

packages/react/test/index.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-ignore-next-line
22
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
33

4-
import { signal, useComputed, useSignalEffect } from "@preact/signals-react";
4+
import { signal, computed, useComputed, useSignalEffect } from "@preact/signals-react";
55
import { createElement, forwardRef, useMemo, memo, StrictMode, createRef } from "react";
66

77
import { createRoot, Root } from "react-dom/client";
@@ -41,6 +41,16 @@ describe("@preact/signals-react", () => {
4141
expect(text).to.have.property("data", "test");
4242
});
4343

44+
it("should render computed as Text", () => {
45+
const sig = signal("test");
46+
const comp = computed(() => `${sig} ${sig}`);
47+
render(<span>{comp}</span>);
48+
const span = scratch.firstChild;
49+
expect(span).to.have.property("firstChild").that.is.an.instanceOf(Text);
50+
const text = span?.firstChild;
51+
expect(text).to.have.property("data", "test test");
52+
});
53+
4454
it("should update Signal-based Text (no parent component)", () => {
4555
const sig = signal("test");
4656
render(<span>{sig}</span>);
@@ -149,7 +159,7 @@ describe("@preact/signals-react", () => {
149159

150160
function App() {
151161
sig.value;
152-
return useMemo(() => <Inner foo={1} />, []);
162+
return useMemo(() => <Inner />, []);
153163
}
154164

155165
render(<App />);

0 commit comments

Comments
 (0)