Skip to content

Commit b158661

Browse files
committed
Explicitly detect other invalid dispatchers separately from ContextOnly and valid dispatchers
1 parent 95d77e1 commit b158661

File tree

1 file changed

+45
-25
lines changed

1 file changed

+45
-25
lines changed

packages/react/src/index.ts

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,20 @@ function createEffectStore(): EffectStore {
106106
// To track when we are entering and exiting a component render (i.e. before and
107107
// after React renders a component), we track how the dispatcher changes.
108108
// Outside of a component rendering, the dispatcher is set to an instance that
109-
// errors or warns when any hooks are called (this is too prevent hooks from
110-
// being used outside of components). Right before React renders a component,
111-
// the dispatcher is set to a valid one. Right after React finishes rendering a
112-
// component, the dispatcher is set to an erroring one again. This erroring
113-
// dispatcher is called `ContextOnlyDispatcher` in React's source.
109+
// errors or warns when any hooks are called. This behavior is prevents hooks
110+
// from being used outside of components. Right before React renders a
111+
// component, the dispatcher is set to a valid one. Right after React finishes
112+
// rendering a component, the dispatcher is set to an erroring one again. This
113+
// erroring dispatcher is called the `ContextOnlyDispatcher` in React's source.
114114
//
115-
// So, we use this getter and setter to monitor the changes to the current
116-
// ReactDispatcher. When the dispatcher changes from the ContextOnlyDispatcher
117-
// to a valid dispatcher, we assume we are entering a component render. At this
118-
// point, we setup our auto-subscriptions for any signals used in the component.
119-
// We do this by creating an effect and manually starting the effect. We use
120-
// `useReducer` to get access to a `rerender` function that we can use to
121-
// manually trigger a rerender when a signal we've subscribed changes.
115+
// So, we watch the getter and setter on `ReactCurrentDispatcher.current` to
116+
// monitor the changes to the current ReactDispatcher. When the dispatcher
117+
// changes from the ContextOnlyDispatcher to a valid dispatcher, we assume we
118+
// are entering a component render. At this point, we setup our
119+
// auto-subscriptions for any signals used in the component. We do this by
120+
// creating an effect and manually starting the effect. We use
121+
// `useSyncExternalStore` to trigger rerenders on the component when any signals
122+
// it uses changes.
122123
//
123124
// When the dispatcher changes from a valid dispatcher back to the
124125
// ContextOnlyDispatcher, we assume we are exiting a component render. At this
@@ -137,13 +138,13 @@ function createEffectStore(): EffectStore {
137138
//
138139
// When a Component's function body invokes useReducer, useState, or useMemo,
139140
// this change in dispatcher should not signal that we are exiting a component
140-
// render. We ignore this change cuz this erroring dispatcher does not pass
141-
// the ContextOnlyDispatcher check and so does not affect our logic.
141+
// render. We ignore this change by detecting these dispatchers as different
142+
// from ContextOnlyDispatcher and other valid dispatchers.
142143
//
143144
// - The `use` hook will change the dispatcher to from a valid update dispatcher
144145
// to a valid mount dispatcher in some cases. Similarly to useReducer
145146
// mentioned above, we should not signal that we are exiting a component
146-
// during this change. Because these other dispatchers do not pass the
147+
// during this change. Because these other valid dispatchers do not pass the
147148
// ContextOnlyDispatcher check, they do not affect our logic.
148149
let lock = false;
149150
let currentDispatcher: ReactDispatcher | null = null;
@@ -157,13 +158,20 @@ Object.defineProperty(ReactInternals.ReactCurrentDispatcher, "current", {
157158
return;
158159
}
159160

161+
const currentDispatcherType = getDispatcherType(currentDispatcher);
162+
const nextDispatcherType = getDispatcherType(nextDispatcher);
163+
164+
// We are entering a component render if the current dispatcher is the
165+
// ContextOnlyDispatcher and the next dispatcher is a valid dispatcher.
160166
const isEnteringComponentRender =
161-
isContextOnlyDispatcher(currentDispatcher) &&
162-
!isContextOnlyDispatcher(nextDispatcher);
167+
currentDispatcherType === ContextOnlyDispatcherType &&
168+
nextDispatcherType === ValidDispatcherType;
163169

170+
// We are exiting a component render if the current dispatcher is a valid
171+
// dispatcher and the next dispatcher is the ContextOnlyDispatcher.
164172
const isExitingComponentRender =
165-
!isContextOnlyDispatcher(currentDispatcher) &&
166-
isContextOnlyDispatcher(nextDispatcher);
173+
currentDispatcherType === ValidDispatcherType &&
174+
nextDispatcherType === ContextOnlyDispatcherType;
167175

168176
// Update the current dispatcher now so the hooks inside of the
169177
// useSyncExternalStore shim get the right dispatcher.
@@ -190,13 +198,17 @@ Object.defineProperty(ReactInternals.ReactCurrentDispatcher, "current", {
190198
},
191199
});
192200

201+
const ValidDispatcherType = 0;
202+
const ContextOnlyDispatcherType = 1;
203+
const ErroringDispatcherType = 2;
204+
193205
// We inject a useSyncExternalStore into every function component via
194206
// CurrentDispatcher. This prevents injecting into anything other than a
195207
// function component render.
196-
const dispatcherTypeCache = new Map();
197-
function isContextOnlyDispatcher(dispatcher: ReactDispatcher | null) {
208+
const dispatcherTypeCache = new Map<ReactDispatcher, number>();
209+
function getDispatcherType(dispatcher: ReactDispatcher | null): number {
198210
// Treat null the same as the ContextOnlyDispatcher.
199-
if (!dispatcher) return true;
211+
if (!dispatcher) return ContextOnlyDispatcherType;
200212

201213
const cached = dispatcherTypeCache.get(dispatcher);
202214
if (cached !== undefined) return cached;
@@ -206,9 +218,17 @@ function isContextOnlyDispatcher(dispatcher: ReactDispatcher | null) {
206218
// for this dispatcher's useCallback implementation to determine if it is a
207219
// ContextOnlyDispatcher. All other dispatchers, erroring or not, define
208220
// functions with arguments and so fail this check.
209-
const isContextOnlyDispatcher = dispatcher.useCallback.length < 2;
210-
dispatcherTypeCache.set(dispatcher, isContextOnlyDispatcher);
211-
return isContextOnlyDispatcher;
221+
let type: number;
222+
if (dispatcher.useCallback.length < 2) {
223+
type = ContextOnlyDispatcherType;
224+
} else if (/Invalid/.test(dispatcher.useCallback as any)) {
225+
type = ErroringDispatcherType;
226+
} else {
227+
type = ValidDispatcherType;
228+
}
229+
230+
dispatcherTypeCache.set(dispatcher, type);
231+
return type;
212232
}
213233

214234
function WrapJsx<T>(jsx: T): T {

0 commit comments

Comments
 (0)