@@ -2,11 +2,12 @@ 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" ;
10
11
import { useSyncExternalStore } from "use-sync-external-store/shim" ;
11
12
import {
12
13
signal ,
@@ -16,65 +17,80 @@ import {
16
17
Signal ,
17
18
type ReadonlySignal ,
18
19
} from "@preact/signals-core" ;
19
- import { Effect , ReactDispatcher } from "./internal" ;
20
+ import type { Effect , JsxRuntimeModule } from "./internal" ;
20
21
21
22
export { signal , computed , batch , effect , Signal , type ReadonlySignal } ;
22
23
23
24
const Empty = [ ] as const ;
24
-
25
- /**
26
- * React uses a different entry-point depending on NODE_ENV env var
27
- */
28
- const __DEV__ = process . env . NODE_ENV !== "production" ;
29
-
30
- /**
31
- * Install a middleware into React.createElement to replace any Signals in props with their value.
32
- * @todo this likely needs to be duplicated for jsx()...
33
- */
34
- const createElement = React . createElement ;
35
- // @ts -ignore-next-line
36
- React . createElement = function ( type , props ) {
37
- if ( typeof type === "string" && props ) {
38
- for ( let i in props ) {
39
- let v = props [ i ] ;
40
- if ( i !== "children" && v instanceof Signal ) {
41
- // createPropUpdater(props, i, v);
42
- props [ i ] = v . value ;
43
- }
44
- }
45
- }
46
- // @ts -ignore-next-line
47
- return createElement . apply ( this , arguments ) ;
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
+ apply ( Component : FunctionComponent , thisArg : any , argumentsList : any ) {
41
+ const store = useMemo ( createEffectStore , Empty ) ;
42
+
43
+ useSyncExternalStore ( store . subscribe , store . getSnapshot , store . getSnapshot ) ;
44
+
45
+ const ends = store . updater . _start ( ) ;
46
+ const kids = Component . apply ( thisArg , argumentsList ) ;
47
+ ends ( ) ;
48
+
49
+ return kids ;
50
+ } ,
48
51
} ;
49
52
50
- /*
51
- // This breaks React's controlled components implementation
52
- function createPropUpdater(props: any, prop: string, signal: Signal) {
53
- let ref = props.ref;
54
- if (!ref) ref = props.ref = React.createRef();
55
- effect(() => {
56
- if (props) props[prop] = signal.value;
57
- let el = ref.current;
58
- if (!el) return; // unsubscribe
59
- (el as any)[prop] = signal.value;
60
- });
61
- props = null;
53
+ function ProxyFunctionalComponent ( Component : FunctionComponent < any > ) {
54
+ return ProxyInstance . get ( Component ) || WrapWithProxy ( Component ) ;
62
55
}
63
- */
56
+ function WrapWithProxy ( Component : FunctionComponent < any > ) {
57
+ if ( SupportsProxy ) {
58
+ const ProxyComponent = new Proxy ( Component , ProxyHandlers ) ;
64
59
65
- let finishUpdate : ( ( ) => void ) | undefined ;
60
+ ProxyInstance . set ( Component , ProxyComponent ) ;
61
+ ProxyInstance . set ( ProxyComponent , ProxyComponent ) ;
66
62
67
- function setCurrentUpdater ( updater ?: Effect ) {
68
- // end tracking for the current update:
69
- if ( finishUpdate ) finishUpdate ( ) ;
70
- // start tracking the new update:
71
- finishUpdate = updater && updater . _start ( ) ;
63
+ return ProxyComponent ;
64
+ }
65
+
66
+ /**
67
+ * Emulate a Proxy if environment doesn't support it.
68
+ *
69
+ * @TODO - unlike Proxy, it's not possible to access the type/Component's
70
+ * static properties this way. Not sure if we want to copy all statics here.
71
+ * Omitting this for now.
72
+ *
73
+ * @example - works with Proxy, doesn't with wrapped function.
74
+ * ```
75
+ * const el = <SomeFunctionalComponent />
76
+ * el.type.someOwnOrInheritedProperty;
77
+ * el.type.defaultProps;
78
+ * ```
79
+ */
80
+ const WrappedComponent = function ( ) {
81
+ return ProxyHandlers . apply ( Component , undefined , arguments ) ;
82
+ } ;
83
+ ProxyInstance . set ( Component , WrappedComponent ) ;
84
+ ProxyInstance . set ( WrappedComponent , WrappedComponent ) ;
85
+
86
+ return WrappedComponent ;
72
87
}
73
88
74
89
/**
75
- * A redux-like store whose store value is a positive 32bit integer (a 'version') to be used with useSyncExternalStore API.
76
- * React (current owner) subscribes to this store and gets a snapshot of the current 'version'.
77
- * Whenever the 'version' changes, we tell React it's time to update the component (call 'onStoreChange').
90
+ * A redux-like store whose store value is a positive 32bit integer (a 'version').
91
+ *
92
+ * React subscribes to this store and gets a snapshot of the current 'version',
93
+ * whenever the 'version' changes, we tell React it's time to update the component (call 'onStoreChange').
78
94
*
79
95
* How we achieve this is by creating a binding with an 'effect', when the `effect._callback' is called,
80
96
* we update our store version and tell React to re-render the component ([1] We don't really care when/how React does it).
@@ -88,24 +104,14 @@ function createEffectStore() {
88
104
let version = 0 ;
89
105
let onChangeNotifyReact : ( ( ) => void ) | undefined ;
90
106
91
- const unsubscribe = effect ( function ( this : Effect ) {
107
+ let unsubscribe = effect ( function ( this : Effect ) {
92
108
updater = this ;
93
109
} ) ;
94
-
95
110
updater . _callback = function ( ) {
96
- if ( ! onChangeNotifyReact ) {
97
- /**
98
- * In dev, lazily unsubscribe self if React isn't subscribed to the store,
99
- * in other words, if the component is not mounted anymore.
100
- *
101
- * We do this to deal with StrictMode double rendering React quirks.
102
- * Only one of the renders is actually mounted.
103
- */
104
- return void unsubscribe ( ) ;
105
- }
111
+ if ( ! onChangeNotifyReact ) return void unsubscribe ( ) ;
106
112
107
113
version = ( version + 1 ) | 0 ;
108
- onChangeNotifyReact ( ) ;
114
+ onChangeNotifyReact ! ( ) ;
109
115
} ;
110
116
111
117
return {
@@ -115,11 +121,9 @@ function createEffectStore() {
115
121
116
122
return function ( ) {
117
123
/**
118
- * In StrictMode (in dev mode), React will play with subscribe/unsubscribe/subscribe in double renders,
119
- * We don't really want to unsubscribe during React's play-time and can't reliably know which of renders
120
- * will end up actually being mounted, so we defer unsubscribe to the updater._callback.
124
+ * @todo - we may want to unsubscribe here instead?
125
+ * Components wrapped in `memo` no longer get updated if we unsubscribe here.
121
126
*/
122
- if ( ! __DEV__ ) unsubscribe ( ) ;
123
127
onChangeNotifyReact = undefined ;
124
128
} ;
125
129
} ,
@@ -129,6 +133,49 @@ function createEffectStore() {
129
133
} ;
130
134
}
131
135
136
+ function WrapJsx < T > ( jsx : T ) : T {
137
+ if ( typeof jsx !== "function" ) return jsx ;
138
+
139
+ return function ( type : any , props : any , ...rest : any [ ] ) {
140
+ if ( typeof type === "function" && ! ( type instanceof Component ) ) {
141
+ return jsx . call ( jsx , ProxyFunctionalComponent ( type ) , props , ...rest ) ;
142
+ }
143
+
144
+ if ( type && typeof type === "object" && type . $$typeof === ReactMemoType ) {
145
+ return jsx . call ( jsx , ProxyFunctionalComponent ( type . type ) , props , ...rest ) ;
146
+ }
147
+
148
+ if ( typeof type === "string" && props ) {
149
+ for ( let i in props ) {
150
+ let v = props [ i ] ;
151
+ if ( i !== "children" && v instanceof Signal ) {
152
+ props [ i ] = v . value ;
153
+ }
154
+ }
155
+ }
156
+
157
+ return jsx . call ( jsx , type , props , ...rest ) ;
158
+ } as any as T ;
159
+ }
160
+
161
+ const JsxPro : JsxRuntimeModule = jsxRuntime ;
162
+ const JsxDev : JsxRuntimeModule = jsxRuntimeDev ;
163
+
164
+ /**
165
+ * createElement _may_ be called by jsx runtime as a fallback in certain cases,
166
+ * so we need to wrap it regardless.
167
+ *
168
+ * The jsx exports depend on the `NODE_ENV` var to ensure the users' bundler doesn't
169
+ * include both, so one of them will be set with `undefined` values.
170
+ */
171
+ React . createElement = WrapJsx ( React . createElement ) ;
172
+ JsxDev . jsx && /* */ ( JsxDev . jsx = WrapJsx ( JsxDev . jsx ) ) ;
173
+ JsxPro . jsx && /* */ ( JsxPro . jsx = WrapJsx ( JsxPro . jsx ) ) ;
174
+ JsxDev . jsxs && /* */ ( JsxDev . jsxs = WrapJsx ( JsxDev . jsxs ) ) ;
175
+ JsxPro . jsxs && /* */ ( JsxPro . jsxs = WrapJsx ( JsxPro . jsxs ) ) ;
176
+ JsxDev . jsxDEV && /**/ ( JsxDev . jsxDEV = WrapJsx ( JsxDev . jsxDEV ) ) ;
177
+ JsxPro . jsxDEV && /**/ ( JsxPro . jsxDEV = WrapJsx ( JsxPro . jsxDEV ) ) ;
178
+
132
179
/**
133
180
* A wrapper component that renders a Signal's value directly as a Text node.
134
181
*/
@@ -137,11 +184,9 @@ function Text({ data }: { data: Signal }) {
137
184
}
138
185
139
186
// Decorate Signals so React renders them as <Text> components.
140
- //@ts -ignore-next-line
141
- const $$typeof = createElement ( "a" ) . $$typeof ;
142
187
Object . defineProperties ( Signal . prototype , {
143
- $$typeof : { configurable : true , value : $$typeof } ,
144
- type : { configurable : true , value : Text } ,
188
+ $$typeof : { configurable : true , value : ReactElemType } ,
189
+ type : { configurable : true , value : ProxyFunctionalComponent ( Text ) } ,
145
190
props : {
146
191
configurable : true ,
147
192
get ( ) {
@@ -151,52 +196,6 @@ Object.defineProperties(Signal.prototype, {
151
196
ref : { configurable : true , value : null } ,
152
197
} ) ;
153
198
154
- // Track the current dispatcher (roughly equiv to current component impl)
155
- let lock = false ;
156
- let currentDispatcher : ReactDispatcher ;
157
-
158
- Object . defineProperty ( internals . ReactCurrentDispatcher , "current" , {
159
- get ( ) {
160
- return currentDispatcher ;
161
- } ,
162
- set ( api : ReactDispatcher ) {
163
- currentDispatcher = api ;
164
- if ( lock ) return ;
165
- if ( api && ! isInvalidHookAccessor ( api ) ) {
166
- // prevent re-injecting useMemo & useSyncExternalStore when the Dispatcher
167
- // context changes.
168
- lock = true ;
169
-
170
- const store = api . useMemo ( createEffectStore , Empty ) ;
171
-
172
- useSyncExternalStore (
173
- store . subscribe ,
174
- store . getSnapshot ,
175
- store . getSnapshot
176
- ) ;
177
-
178
- lock = false ;
179
-
180
- setCurrentUpdater ( store . updater ) ;
181
- } else {
182
- setCurrentUpdater ( ) ;
183
- }
184
- } ,
185
- } ) ;
186
-
187
- // We inject a useReducer into every function component via CurrentDispatcher.
188
- // This prevents injecting into anything other than a function component render.
189
- const invalidHookAccessors = new Map ( ) ;
190
- function isInvalidHookAccessor ( api : ReactDispatcher ) {
191
- const cached = invalidHookAccessors . get ( api ) ;
192
- if ( cached !== undefined ) return cached ;
193
- // we only want the real implementation, not the warning ones
194
- const invalid =
195
- api . useCallback . length < 2 || / I n v a l i d / . test ( api . useCallback as any ) ;
196
- invalidHookAccessors . set ( api , invalid ) ;
197
- return invalid ;
198
- }
199
-
200
199
export function useSignal < T > ( value : T ) {
201
200
return useMemo ( ( ) => signal < T > ( value ) , Empty ) ;
202
201
}
0 commit comments