@@ -2,11 +2,13 @@ import {
2
2
useRef ,
3
3
useMemo ,
4
4
useEffect ,
5
- // @ts -ignore-next-line
6
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
- __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED as internals ,
5
+ Component ,
6
+ type FunctionComponent ,
8
7
} from "react" ;
9
8
import React from "react" ;
9
+ import jsxRuntime from "react/jsx-runtime" ;
10
+ import jsxRuntimeDev from "react/jsx-dev-runtime" ;
11
+ import { useSyncExternalStore } from "use-sync-external-store/shim" ;
10
12
import {
11
13
signal ,
12
14
computed ,
@@ -15,64 +17,183 @@ import {
15
17
Signal ,
16
18
type ReadonlySignal ,
17
19
} from "@preact/signals-core" ;
18
- import { Effect , ReactDispatcher } from "./internal" ;
20
+ import type { Effect , JsxRuntimeModule } from "./internal" ;
19
21
20
22
export { signal , computed , batch , effect , Signal , type ReadonlySignal } ;
21
23
22
- /**
23
- * Install a middleware into React.createElement to replace any Signals in props with their value.
24
- * @todo this likely needs to be duplicated for jsx()...
25
- */
26
- const createElement = React . createElement ;
27
- // @ts -ignore-next-line
28
- React . createElement = function ( type , props ) {
29
- if ( typeof type === "string" && props ) {
30
- for ( let i in props ) {
31
- let v = props [ i ] ;
32
- if ( i !== "children" && v instanceof Signal ) {
33
- // createPropUpdater(props, i, v);
34
- props [ i ] = v . value ;
35
- }
24
+ const Empty = [ ] as const ;
25
+ const ReactElemType = Symbol . for ( "react.element" ) ; // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L15
26
+ const ReactMemoType = Symbol . for ( "react.memo" ) ; // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L30
27
+ const ProxyInstance = new Map < FunctionComponent < any > , FunctionComponent < any > > ( ) ;
28
+ const SupportsProxy = typeof Proxy === "function" ;
29
+
30
+ const ProxyHandlers = {
31
+ /**
32
+ * This is a function call trap for functional components.
33
+ * When this is called, we know it means React did run 'Component()',
34
+ * that means we can use any hooks here to setup our effect and store.
35
+ *
36
+ * With the native Proxy, all other calls such as access/setting to/of properties will
37
+ * be forwarded to the target Component, so we don't need to copy the Component's
38
+ * own or inherited properties.
39
+ *
40
+ * @see https://github.com/facebook/react/blob/2d80a0cd690bb5650b6c8a6c079a87b5dc42bd15/packages/react-reconciler/src/ReactFiberHooks.old.js#L460
41
+ */
42
+ apply ( Component : FunctionComponent , thisArg : any , argumentsList : any ) {
43
+ const store = useMemo ( createEffectStore , Empty ) ;
44
+
45
+ useSyncExternalStore ( store . subscribe , store . getSnapshot , store . getSnapshot ) ;
46
+
47
+ const stop = store . updater . _start ( ) ;
48
+
49
+ try {
50
+ const children = Component . apply ( thisArg , argumentsList ) ;
51
+ return children ;
52
+ } catch ( e ) {
53
+ // Re-throwing promises that'll be handled by suspense
54
+ // or an actual error.
55
+ throw e ;
56
+ } finally {
57
+ // Stop effects in either case before return or throw,
58
+ // Otherwise the effect will leak.
59
+ stop ( ) ;
36
60
}
37
- }
38
- // @ts -ignore-next-line
39
- return createElement . apply ( this , arguments ) ;
61
+ } ,
40
62
} ;
41
63
42
- /*
43
- // This breaks React's controlled components implementation
44
- function createPropUpdater(props: any, prop: string, signal: Signal) {
45
- let ref = props.ref;
46
- if (!ref) ref = props.ref = React.createRef();
47
- effect(() => {
48
- if (props) props[prop] = signal.value;
49
- let el = ref.current;
50
- if (!el) return; // unsubscribe
51
- (el as any)[prop] = signal.value;
52
- });
53
- props = null;
64
+ function ProxyFunctionalComponent ( Component : FunctionComponent < any > ) {
65
+ return ProxyInstance . get ( Component ) || WrapWithProxy ( Component ) ;
54
66
}
55
- */
67
+ function WrapWithProxy ( Component : FunctionComponent < any > ) {
68
+ if ( SupportsProxy ) {
69
+ const ProxyComponent = new Proxy ( Component , ProxyHandlers ) ;
56
70
57
- let finishUpdate : ( ( ) => void ) | undefined ;
58
- const updaterForComponent = new WeakMap < ( ) => void , Effect > ( ) ;
71
+ ProxyInstance . set ( Component , ProxyComponent ) ;
72
+ ProxyInstance . set ( ProxyComponent , ProxyComponent ) ;
59
73
60
- function setCurrentUpdater ( updater ?: Effect ) {
61
- // end tracking for the current update:
62
- if ( finishUpdate ) finishUpdate ( ) ;
63
- // start tracking the new update:
64
- finishUpdate = updater && updater . _start ( ) ;
74
+ return ProxyComponent ;
75
+ }
76
+
77
+ /**
78
+ * Emulate a Proxy if environment doesn't support it.
79
+ *
80
+ * @TODO - unlike Proxy, it's not possible to access the type/Component's
81
+ * static properties this way. Not sure if we want to copy all statics here.
82
+ * Omitting this for now.
83
+ *
84
+ * @example - works with Proxy, doesn't with wrapped function.
85
+ * ```
86
+ * const el = <SomeFunctionalComponent />
87
+ * el.type.someOwnOrInheritedProperty;
88
+ * el.type.defaultProps;
89
+ * ```
90
+ */
91
+ const WrappedComponent = function ( ) {
92
+ return ProxyHandlers . apply ( Component , undefined , arguments ) ;
93
+ } ;
94
+ ProxyInstance . set ( Component , WrappedComponent ) ;
95
+ ProxyInstance . set ( WrappedComponent , WrappedComponent ) ;
96
+
97
+ return WrappedComponent ;
65
98
}
66
99
67
- function createUpdater ( update : ( ) => void ) {
100
+ /**
101
+ * A redux-like store whose store value is a positive 32bit integer (a 'version').
102
+ *
103
+ * React subscribes to this store and gets a snapshot of the current 'version',
104
+ * whenever the 'version' changes, we tell React it's time to update the component (call 'onStoreChange').
105
+ *
106
+ * How we achieve this is by creating a binding with an 'effect', when the `effect._callback' is called,
107
+ * we update our store version and tell React to re-render the component ([1] We don't really care when/how React does it).
108
+ *
109
+ * [1]
110
+ * @see https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore
111
+ * @see https://github.com/reactjs/rfcs/blob/main/text/0214-use-sync-external-store.md
112
+ */
113
+ function createEffectStore ( ) {
68
114
let updater ! : Effect ;
69
- effect ( function ( this : Effect ) {
115
+ let version = 0 ;
116
+ let onChangeNotifyReact : ( ( ) => void ) | undefined ;
117
+
118
+ let unsubscribe = effect ( function ( this : Effect ) {
70
119
updater = this ;
71
120
} ) ;
72
- updater . _callback = update ;
73
- return updater ;
121
+ updater . _callback = function ( ) {
122
+ version = ( version + 1 ) | 0 ;
123
+ if ( onChangeNotifyReact ) onChangeNotifyReact ( ) ;
124
+ } ;
125
+
126
+ return {
127
+ updater,
128
+ subscribe ( onStoreChange : ( ) => void ) {
129
+ onChangeNotifyReact = onStoreChange ;
130
+
131
+ return function ( ) {
132
+ /**
133
+ * Rotate to next version when unsubscribing to ensure that components are re-run
134
+ * when subscribing again.
135
+ *
136
+ * In StrictMode, 'memo'-ed components seem to keep a stale snapshot version, so
137
+ * don't re-run after subscribing again if the version is the same as last time.
138
+ *
139
+ * Because we unsubscribe from the effect, the version may not change. We simply
140
+ * set a new initial version in case of stale snapshots here.
141
+ */
142
+ version = ( version + 1 ) | 0 ;
143
+ onChangeNotifyReact = undefined ;
144
+ unsubscribe ( ) ;
145
+ } ;
146
+ } ,
147
+ getSnapshot ( ) {
148
+ return version ;
149
+ } ,
150
+ } ;
151
+ }
152
+
153
+ function WrapJsx < T > ( jsx : T ) : T {
154
+ if ( typeof jsx !== "function" ) return jsx ;
155
+
156
+ return function ( type : any , props : any , ...rest : any [ ] ) {
157
+ if ( typeof type === "function" && ! ( type instanceof Component ) ) {
158
+ return jsx . call ( jsx , ProxyFunctionalComponent ( type ) , props , ...rest ) ;
159
+ }
160
+
161
+ if ( type && typeof type === "object" && type . $$typeof === ReactMemoType ) {
162
+ type . type = ProxyFunctionalComponent ( type . type ) ;
163
+ return jsx . call ( jsx , type , props , ...rest ) ;
164
+ }
165
+
166
+ if ( typeof type === "string" && props ) {
167
+ for ( let i in props ) {
168
+ let v = props [ i ] ;
169
+ if ( i !== "children" && v instanceof Signal ) {
170
+ props [ i ] = v . value ;
171
+ }
172
+ }
173
+ }
174
+
175
+ return jsx . call ( jsx , type , props , ...rest ) ;
176
+ } as any as T ;
74
177
}
75
178
179
+ const JsxPro : JsxRuntimeModule = jsxRuntime ;
180
+ const JsxDev : JsxRuntimeModule = jsxRuntimeDev ;
181
+
182
+ /**
183
+ * createElement _may_ be called by jsx runtime as a fallback in certain cases,
184
+ * so we need to wrap it regardless.
185
+ *
186
+ * The jsx exports depend on the `NODE_ENV` var to ensure the users' bundler doesn't
187
+ * include both, so one of them will be set with `undefined` values.
188
+ */
189
+ React . createElement = WrapJsx ( React . createElement ) ;
190
+ JsxDev . jsx && /* */ ( JsxDev . jsx = WrapJsx ( JsxDev . jsx ) ) ;
191
+ JsxPro . jsx && /* */ ( JsxPro . jsx = WrapJsx ( JsxPro . jsx ) ) ;
192
+ JsxDev . jsxs && /* */ ( JsxDev . jsxs = WrapJsx ( JsxDev . jsxs ) ) ;
193
+ JsxPro . jsxs && /* */ ( JsxPro . jsxs = WrapJsx ( JsxPro . jsxs ) ) ;
194
+ JsxDev . jsxDEV && /**/ ( JsxDev . jsxDEV = WrapJsx ( JsxDev . jsxDEV ) ) ;
195
+ JsxPro . jsxDEV && /**/ ( JsxPro . jsxDEV = WrapJsx ( JsxPro . jsxDEV ) ) ;
196
+
76
197
/**
77
198
* A wrapper component that renders a Signal's value directly as a Text node.
78
199
*/
@@ -81,11 +202,9 @@ function Text({ data }: { data: Signal }) {
81
202
}
82
203
83
204
// Decorate Signals so React renders them as <Text> components.
84
- //@ts -ignore-next-line
85
- const $$typeof = createElement ( "a" ) . $$typeof ;
86
205
Object . defineProperties ( Signal . prototype , {
87
- $$typeof : { configurable : true , value : $$typeof } ,
88
- type : { configurable : true , value : Text } ,
206
+ $$typeof : { configurable : true , value : ReactElemType } ,
207
+ type : { configurable : true , value : ProxyFunctionalComponent ( Text ) } ,
89
208
props : {
90
209
configurable : true ,
91
210
get ( ) {
@@ -95,59 +214,14 @@ Object.defineProperties(Signal.prototype, {
95
214
ref : { configurable : true , value : null } ,
96
215
} ) ;
97
216
98
- // Track the current dispatcher (roughly equiv to current component impl)
99
- let lock = false ;
100
- const UPDATE = ( ) => ( { } ) ;
101
- let currentDispatcher : ReactDispatcher ;
102
- Object . defineProperty ( internals . ReactCurrentDispatcher , "current" , {
103
- get ( ) {
104
- return currentDispatcher ;
105
- } ,
106
- set ( api ) {
107
- currentDispatcher = api ;
108
- if ( lock ) return ;
109
- if ( api && ! isInvalidHookAccessor ( api ) ) {
110
- // prevent re-injecting useReducer when the Dispatcher
111
- // context changes to run the reducer callback:
112
- lock = true ;
113
- const rerender = api . useReducer ( UPDATE , { } ) [ 1 ] ;
114
- lock = false ;
115
-
116
- let updater = updaterForComponent . get ( rerender ) ;
117
- if ( ! updater ) {
118
- updater = createUpdater ( rerender ) ;
119
- updaterForComponent . set ( rerender , updater ) ;
120
- } else {
121
- updater . _callback = rerender ;
122
- }
123
- setCurrentUpdater ( updater ) ;
124
- } else {
125
- setCurrentUpdater ( ) ;
126
- }
127
- } ,
128
- } ) ;
129
-
130
- // We inject a useReducer into every function component via CurrentDispatcher.
131
- // This prevents injecting into anything other than a function component render.
132
- const invalidHookAccessors = new Map ( ) ;
133
- function isInvalidHookAccessor ( api : ReactDispatcher ) {
134
- const cached = invalidHookAccessors . get ( api ) ;
135
- if ( cached !== undefined ) return cached ;
136
- // we only want the real implementation, not the warning ones
137
- const invalid =
138
- api . useCallback . length < 2 || / I n v a l i d / . test ( api . useCallback as any ) ;
139
- invalidHookAccessors . set ( api , invalid ) ;
140
- return invalid ;
141
- }
142
-
143
217
export function useSignal < T > ( value : T ) {
144
- return useMemo ( ( ) => signal < T > ( value ) , [ ] ) ;
218
+ return useMemo ( ( ) => signal < T > ( value ) , Empty ) ;
145
219
}
146
220
147
221
export function useComputed < T > ( compute : ( ) => T ) {
148
222
const $compute = useRef ( compute ) ;
149
223
$compute . current = compute ;
150
- return useMemo ( ( ) => computed < T > ( ( ) => $compute . current ( ) ) , [ ] ) ;
224
+ return useMemo ( ( ) => computed < T > ( ( ) => $compute . current ( ) ) , Empty ) ;
151
225
}
152
226
153
227
export function useSignalEffect ( cb : ( ) => void | ( ( ) => void ) ) {
@@ -158,5 +232,5 @@ export function useSignalEffect(cb: () => void | (() => void)) {
158
232
return effect ( ( ) => {
159
233
return callback . current ( ) ;
160
234
} ) ;
161
- } , [ ] ) ;
235
+ } , Empty ) ;
162
236
}
0 commit comments