Skip to content

Commit 2ac527b

Browse files
authored
RSC-specific workarounds (#2050)
* map context instances using `createContext` as key * use `{}` as ReactReduxContext in environments where no context exists (removes `Proxy` use) * switch React imports to namespace imports to prevent hook import detection * use `globalThis` only when availbalbe
1 parent a669a94 commit 2ac527b

File tree

6 files changed

+48
-47
lines changed

6 files changed

+48
-47
lines changed

Diff for: src/components/Context.ts

+19-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, version as ReactVersion } from 'react'
1+
import * as React from 'react'
22
import type { Context } from 'react'
33
import type { Action, AnyAction, Store } from 'redux'
44
import type { Subscription } from '../utils/Subscription'
@@ -15,34 +15,33 @@ export interface ReactReduxContextValue<
1515
noopCheck: CheckFrequency
1616
}
1717

18-
const ContextKey = Symbol.for(`react-redux-context-${ReactVersion}`)
19-
const gT = globalThis as { [ContextKey]?: Context<ReactReduxContextValue> }
18+
const ContextKey = Symbol.for(`react-redux-context`)
19+
const gT: {
20+
[ContextKey]?: Map<
21+
typeof React.createContext,
22+
Context<ReactReduxContextValue>
23+
>
24+
} = (typeof globalThis !== "undefined" ? globalThis : /* fall back to a per-module scope (pre-8.1 behaviour) if `globalThis` is not available */ {}) as any;
2025

21-
function getContext() {
22-
let realContext = gT[ContextKey]
26+
function getContext(): Context<ReactReduxContextValue> {
27+
if (!React.createContext) return {} as any
28+
29+
const contextMap = (gT[ContextKey] ??= new Map<
30+
typeof React.createContext,
31+
Context<ReactReduxContextValue>
32+
>())
33+
let realContext = contextMap.get(React.createContext)
2334
if (!realContext) {
24-
realContext = createContext<ReactReduxContextValue>(null as any)
35+
realContext = React.createContext<ReactReduxContextValue>(null as any)
2536
if (process.env.NODE_ENV !== 'production') {
2637
realContext.displayName = 'ReactRedux'
2738
}
28-
gT[ContextKey] = realContext
39+
contextMap.set(React.createContext, realContext)
2940
}
3041
return realContext
3142
}
3243

33-
export const ReactReduxContext = /*#__PURE__*/ new Proxy(
34-
{} as Context<ReactReduxContextValue>,
35-
/*#__PURE__*/ new Proxy<ProxyHandler<Context<ReactReduxContextValue>>>(
36-
{},
37-
{
38-
get(_, handler) {
39-
const target = getContext()
40-
// @ts-ignore
41-
return (_target, ...args) => Reflect[handler](target, ...args)
42-
},
43-
}
44-
)
45-
)
44+
export const ReactReduxContext = /*#__PURE__*/ getContext()
4645

4746
export type ReactReduxContextInstance = typeof ReactReduxContext
4847

Diff for: src/components/Provider.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Context, ReactNode } from 'react'
2-
import React, { useMemo } from 'react'
2+
import * as React from 'react'
33
import type { ReactReduxContextValue } from './Context'
44
import { ReactReduxContext } from './Context'
55
import { createSubscription } from '../utils/Subscription'
@@ -42,7 +42,7 @@ function Provider<A extends Action = AnyAction, S = unknown>({
4242
stabilityCheck = 'once',
4343
noopCheck = 'once',
4444
}: ProviderProps<A, S>) {
45-
const contextValue = useMemo(() => {
45+
const contextValue = React.useMemo(() => {
4646
const subscription = createSubscription(store)
4747
return {
4848
store,
@@ -53,7 +53,7 @@ function Provider<A extends Action = AnyAction, S = unknown>({
5353
}
5454
}, [store, serverState, stabilityCheck, noopCheck])
5555

56-
const previousState = useMemo(() => store.getState(), [store])
56+
const previousState = React.useMemo(() => store.getState(), [store])
5757

5858
useIsomorphicLayoutEffect(() => {
5959
const { subscription } = contextValue

Diff for: src/components/connect.tsx

+18-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */
22
import hoistStatics from 'hoist-non-react-statics'
33
import type { ComponentType } from 'react'
4-
import React, { useContext, useMemo, useRef } from 'react'
4+
import * as React from 'react'
55
import { isValidElementType, isContextConsumer } from 'react-is'
66

77
import type { Store } from 'redux'
@@ -533,15 +533,15 @@ function connect<
533533
props: InternalConnectProps & TOwnProps
534534
) {
535535
const [propsContext, reactReduxForwardedRef, wrapperProps] =
536-
useMemo(() => {
536+
React.useMemo(() => {
537537
// Distinguish between actual "data" props that were passed to the wrapper component,
538538
// and values needed to control behavior (forwarded refs, alternate context instances).
539539
// To maintain the wrapperProps object reference, memoize this destructuring.
540540
const { reactReduxForwardedRef, ...wrapperProps } = props
541541
return [props.context, reactReduxForwardedRef, wrapperProps]
542542
}, [props])
543543

544-
const ContextToUse: ReactReduxContextInstance = useMemo(() => {
544+
const ContextToUse: ReactReduxContextInstance = React.useMemo(() => {
545545
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
546546
// Memoize the check that determines which context instance we should use.
547547
return propsContext &&
@@ -553,7 +553,7 @@ function connect<
553553
}, [propsContext, Context])
554554

555555
// Retrieve the store and ancestor subscription via context, if available
556-
const contextValue = useContext(ContextToUse)
556+
const contextValue = React.useContext(ContextToUse)
557557

558558
// The store _must_ exist as either a prop or in context.
559559
// We'll check to see if it _looks_ like a Redux store first.
@@ -587,13 +587,13 @@ function connect<
587587
? contextValue.getServerState
588588
: store.getState
589589

590-
const childPropsSelector = useMemo(() => {
590+
const childPropsSelector = React.useMemo(() => {
591591
// The child props selector needs the store reference as an input.
592592
// Re-create this selector whenever the store changes.
593593
return defaultSelectorFactory(store.dispatch, selectorFactoryOptions)
594594
}, [store])
595595

596-
const [subscription, notifyNestedSubs] = useMemo(() => {
596+
const [subscription, notifyNestedSubs] = React.useMemo(() => {
597597
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
598598

599599
// This Subscription's source should match where store came from: props vs. context. A component
@@ -615,7 +615,7 @@ function connect<
615615

616616
// Determine what {store, subscription} value should be put into nested context, if necessary,
617617
// and memoize that value to avoid unnecessary context updates.
618-
const overriddenContextValue = useMemo(() => {
618+
const overriddenContextValue = React.useMemo(() => {
619619
if (didStoreComeFromProps) {
620620
// This component is directly subscribed to a store from props.
621621
// We don't want descendants reading from this store - pass down whatever
@@ -632,14 +632,14 @@ function connect<
632632
}, [didStoreComeFromProps, contextValue, subscription])
633633

634634
// Set up refs to coordinate values between the subscription effect and the render logic
635-
const lastChildProps = useRef<unknown>()
636-
const lastWrapperProps = useRef(wrapperProps)
637-
const childPropsFromStoreUpdate = useRef<unknown>()
638-
const renderIsScheduled = useRef(false)
639-
const isProcessingDispatch = useRef(false)
640-
const isMounted = useRef(false)
635+
const lastChildProps = React.useRef<unknown>()
636+
const lastWrapperProps = React.useRef(wrapperProps)
637+
const childPropsFromStoreUpdate = React.useRef<unknown>()
638+
const renderIsScheduled = React.useRef(false)
639+
const isProcessingDispatch = React.useRef(false)
640+
const isMounted = React.useRef(false)
641641

642-
const latestSubscriptionCallbackError = useRef<Error>()
642+
const latestSubscriptionCallbackError = React.useRef<Error>()
643643

644644
useIsomorphicLayoutEffect(() => {
645645
isMounted.current = true
@@ -648,7 +648,7 @@ function connect<
648648
}
649649
}, [])
650650

651-
const actualChildPropsSelector = useMemo(() => {
651+
const actualChildPropsSelector = React.useMemo(() => {
652652
const selector = () => {
653653
// Tricky logic here:
654654
// - This render may have been triggered by a Redux store update that produced new child props
@@ -676,7 +676,7 @@ function connect<
676676
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
677677
// just useEffect instead to avoid the warning, since neither will run anyway.
678678

679-
const subscribeForReact = useMemo(() => {
679+
const subscribeForReact = React.useMemo(() => {
680680
const subscribe = (reactListener: () => void) => {
681681
if (!subscription) {
682682
return () => {}
@@ -741,7 +741,7 @@ function connect<
741741

742742
// Now that all that's done, we can finally try to actually render the child component.
743743
// We memoize the elements for the rendered child component as an optimization.
744-
const renderedWrappedComponent = useMemo(() => {
744+
const renderedWrappedComponent = React.useMemo(() => {
745745
return (
746746
// @ts-ignore
747747
<WrappedComponent
@@ -753,7 +753,7 @@ function connect<
753753

754754
// If React sees the exact same element reference as last time, it bails out of re-rendering
755755
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
756-
const renderedChild = useMemo(() => {
756+
const renderedChild = React.useMemo(() => {
757757
if (shouldHandleStateChanges) {
758758
// If this component is subscribed to store updates, we need to pass its own
759759
// subscription instance down to our descendants. That means rendering the same

Diff for: src/next.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// The useSyncExternalStoreWithSelector has to be imported, but we can use the
44
// non-shim version. This shaves off the byte size of the shim.
55

6-
import { useSyncExternalStore } from 'react'
6+
import * as React from 'react'
77
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector'
88

99
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
@@ -13,7 +13,7 @@ import { initializeUseSelector } from './hooks/useSelector'
1313
import { initializeConnect } from './components/connect'
1414

1515
initializeUseSelector(useSyncExternalStoreWithSelector)
16-
initializeConnect(useSyncExternalStore)
16+
initializeConnect(React.useSyncExternalStore)
1717

1818
// Enable batched updates in our subscriptions for use
1919
// with standard React renderers (ReactDOM, React Native)

Diff for: src/utils/useIsomorphicLayoutEffect.native.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useLayoutEffect } from 'react'
1+
import * as React from 'react'
22

33
// Under React Native, we know that we always want to use useLayoutEffect
44

5-
export const useIsomorphicLayoutEffect = useLayoutEffect
5+
export const useIsomorphicLayoutEffect = React.useLayoutEffect

Diff for: src/utils/useIsomorphicLayoutEffect.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useLayoutEffect } from 'react'
1+
import * as React from 'react'
22

33
// React currently throws a warning when using useLayoutEffect on the server.
44
// To get around it, we can conditionally useEffect on the server (no-op) and
@@ -16,4 +16,6 @@ export const canUseDOM = !!(
1616
typeof window.document.createElement !== 'undefined'
1717
)
1818

19-
export const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect
19+
export const useIsomorphicLayoutEffect = canUseDOM
20+
? React.useLayoutEffect
21+
: React.useEffect

0 commit comments

Comments
 (0)