Skip to content

Commit fb6b050

Browse files
authored
Fix rendering signals as text when using react-transform (#439)
* On exported useSignals, only run logic if auto is not installed * Remove top-level requirement from react-transform * Add return types to react runtime exports
1 parent beafe69 commit fb6b050

File tree

5 files changed

+65
-14
lines changed

5 files changed

+65
-14
lines changed

.changeset/fresh-panthers-explain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals-react-transform": patch
3+
---
4+
5+
Remove top-level requirement from react-transform

.changeset/lovely-moons-beam.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+
Fix rendering signals as text when using react-transform

packages/react-transform/src/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,12 @@ function isOptedOutOfSignalTracking(path: NodePath | null): boolean {
145145
function isComponentFunction(path: NodePath<FunctionLike>): boolean {
146146
return (
147147
fnNameStartsWithCapital(path) && // Function name indicates it's a component
148-
getData(path.scope, containsJSX) === true && // Function contains JSX
149-
path.scope.parent === path.scope.getProgramParent() // Function is top-level
148+
getData(path.scope, containsJSX) === true // Function contains JSX
150149
);
151150
}
152151

153152
function isCustomHook(path: NodePath<FunctionLike>): boolean {
154-
return (
155-
fnNameStartsWithUse(path) && // Function name indicates it's a hook
156-
path.scope.parent === path.scope.getProgramParent()
157-
); // Function is top-level
153+
return fnNameStartsWithUse(path); // Function name indicates it's a hook
158154
}
159155

160156
function shouldTransform(

packages/react/runtime/src/auto.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import React from "react";
77
import jsxRuntime from "react/jsx-runtime";
88
import jsxRuntimeDev from "react/jsx-dev-runtime";
9-
import { EffectStore, useSignals, wrapJsx } from "./index";
9+
import { EffectStore, wrapJsx, _useSignalsImplementation } from "./index";
1010

1111
export interface ReactDispatcher {
1212
useRef: typeof React.useRef;
@@ -146,11 +146,15 @@ const dispatcherMachinePROD = createMachine({
146146
```
147147
*/
148148

149+
export let isAutoSignalTrackingInstalled = false;
150+
149151
let store: EffectStore | null = null;
150152
let lock = false;
151153
let currentDispatcher: ReactDispatcher | null = null;
152154

153155
function installCurrentDispatcherHook() {
156+
isAutoSignalTrackingInstalled = true;
157+
154158
Object.defineProperty(ReactInternals.ReactCurrentDispatcher, "current", {
155159
get() {
156160
return currentDispatcher;
@@ -171,7 +175,7 @@ function installCurrentDispatcherHook() {
171175
isEnteringComponentRender(currentDispatcherType, nextDispatcherType)
172176
) {
173177
lock = true;
174-
store = useSignals();
178+
store = _useSignalsImplementation();
175179
lock = false;
176180
} else if (
177181
isExitingComponentRender(currentDispatcherType, nextDispatcherType)

packages/react/runtime/src/index.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import { signal, computed, effect, Signal } from "@preact/signals-core";
1+
import {
2+
signal,
3+
computed,
4+
effect,
5+
Signal,
6+
ReadonlySignal,
7+
} from "@preact/signals-core";
28
import { useRef, useMemo, useEffect } from "react";
39
import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
10+
import { isAutoSignalTrackingInstalled } from "./auto";
411

512
export { installAutoSignalTracking } from "./auto";
613

714
const Empty = [] as const;
815
const ReactElemType = Symbol.for("react.element"); // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L15
16+
const noop = () => {};
917

1018
export function wrapJsx<T>(jsx: T): T {
1119
if (typeof jsx !== "function") return jsx;
@@ -113,14 +121,37 @@ function createEffectStore(): EffectStore {
113121
};
114122
}
115123

124+
function createEmptyEffectStore(): EffectStore {
125+
return {
126+
effect: {
127+
_sources: undefined,
128+
_callback() {},
129+
_start() {
130+
return noop;
131+
},
132+
_dispose() {},
133+
},
134+
subscribe() {
135+
return noop;
136+
},
137+
getSnapshot() {
138+
return 0;
139+
},
140+
f() {},
141+
[symDispose]() {},
142+
};
143+
}
144+
145+
const emptyEffectStore = createEmptyEffectStore();
146+
116147
let finalCleanup: Promise<void> | undefined;
117148
const _queueMicroTask = Promise.prototype.then.bind(Promise.resolve());
118149

119150
/**
120151
* Custom hook to create the effect to track signals used during render and
121152
* subscribe to changes to rerender the component when the signals change.
122153
*/
123-
export function useSignals(): EffectStore {
154+
export function _useSignalsImplementation(): EffectStore {
124155
clearCurrentStore();
125156
if (!finalCleanup) {
126157
finalCleanup = _queueMicroTask(() => {
@@ -145,7 +176,12 @@ export function useSignals(): EffectStore {
145176
* A wrapper component that renders a Signal's value directly as a Text node or JSX.
146177
*/
147178
function SignalValue({ data }: { data: Signal }) {
148-
return data.value;
179+
const store = useSignals();
180+
try {
181+
return data.value;
182+
} finally {
183+
store.f();
184+
}
149185
}
150186

151187
// Decorate Signals so React renders them as <SignalValue> components.
@@ -161,17 +197,22 @@ Object.defineProperties(Signal.prototype, {
161197
ref: { configurable: true, value: null },
162198
});
163199

164-
export function useSignal<T>(value: T) {
200+
export function useSignals(): EffectStore {
201+
if (isAutoSignalTrackingInstalled) return emptyEffectStore;
202+
return _useSignalsImplementation();
203+
}
204+
205+
export function useSignal<T>(value: T): Signal<T> {
165206
return useMemo(() => signal<T>(value), Empty);
166207
}
167208

168-
export function useComputed<T>(compute: () => T) {
209+
export function useComputed<T>(compute: () => T): ReadonlySignal<T> {
169210
const $compute = useRef(compute);
170211
$compute.current = compute;
171212
return useMemo(() => computed<T>(() => $compute.current()), Empty);
172213
}
173214

174-
export function useSignalEffect(cb: () => void | (() => void)) {
215+
export function useSignalEffect(cb: () => void | (() => void)): void {
175216
const callback = useRef(cb);
176217
callback.current = cb;
177218

0 commit comments

Comments
 (0)