Skip to content

Commit

Permalink
RSC-specific workarounds (#2050)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
phryneas committed Jul 29, 2023
1 parent a669a94 commit 2ac527b
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 47 deletions.
39 changes: 19 additions & 20 deletions src/components/Context.ts
@@ -1,4 +1,4 @@
import { createContext, version as ReactVersion } from 'react'
import * as React from 'react'
import type { Context } from 'react'
import type { Action, AnyAction, Store } from 'redux'
import type { Subscription } from '../utils/Subscription'
Expand All @@ -15,34 +15,33 @@ export interface ReactReduxContextValue<
noopCheck: CheckFrequency
}

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

function getContext() {
let realContext = gT[ContextKey]
function getContext(): Context<ReactReduxContextValue> {
if (!React.createContext) return {} as any

const contextMap = (gT[ContextKey] ??= new Map<
typeof React.createContext,
Context<ReactReduxContextValue>
>())
let realContext = contextMap.get(React.createContext)
if (!realContext) {
realContext = createContext<ReactReduxContextValue>(null as any)
realContext = React.createContext<ReactReduxContextValue>(null as any)
if (process.env.NODE_ENV !== 'production') {
realContext.displayName = 'ReactRedux'
}
gT[ContextKey] = realContext
contextMap.set(React.createContext, realContext)
}
return realContext
}

export const ReactReduxContext = /*#__PURE__*/ new Proxy(
{} as Context<ReactReduxContextValue>,
/*#__PURE__*/ new Proxy<ProxyHandler<Context<ReactReduxContextValue>>>(
{},
{
get(_, handler) {
const target = getContext()
// @ts-ignore
return (_target, ...args) => Reflect[handler](target, ...args)
},
}
)
)
export const ReactReduxContext = /*#__PURE__*/ getContext()

export type ReactReduxContextInstance = typeof ReactReduxContext

Expand Down
6 changes: 3 additions & 3 deletions src/components/Provider.tsx
@@ -1,5 +1,5 @@
import type { Context, ReactNode } from 'react'
import React, { useMemo } from 'react'
import * as React from 'react'
import type { ReactReduxContextValue } from './Context'
import { ReactReduxContext } from './Context'
import { createSubscription } from '../utils/Subscription'
Expand Down Expand Up @@ -42,7 +42,7 @@ function Provider<A extends Action = AnyAction, S = unknown>({
stabilityCheck = 'once',
noopCheck = 'once',
}: ProviderProps<A, S>) {
const contextValue = useMemo(() => {
const contextValue = React.useMemo(() => {
const subscription = createSubscription(store)
return {
store,
Expand All @@ -53,7 +53,7 @@ function Provider<A extends Action = AnyAction, S = unknown>({
}
}, [store, serverState, stabilityCheck, noopCheck])

const previousState = useMemo(() => store.getState(), [store])
const previousState = React.useMemo(() => store.getState(), [store])

useIsomorphicLayoutEffect(() => {
const { subscription } = contextValue
Expand Down
36 changes: 18 additions & 18 deletions src/components/connect.tsx
@@ -1,7 +1,7 @@
/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */
import hoistStatics from 'hoist-non-react-statics'
import type { ComponentType } from 'react'
import React, { useContext, useMemo, useRef } from 'react'
import * as React from 'react'
import { isValidElementType, isContextConsumer } from 'react-is'

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

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

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

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

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

const [subscription, notifyNestedSubs] = useMemo(() => {
const [subscription, notifyNestedSubs] = React.useMemo(() => {
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY

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

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

// Set up refs to coordinate values between the subscription effect and the render logic
const lastChildProps = useRef<unknown>()
const lastWrapperProps = useRef(wrapperProps)
const childPropsFromStoreUpdate = useRef<unknown>()
const renderIsScheduled = useRef(false)
const isProcessingDispatch = useRef(false)
const isMounted = useRef(false)
const lastChildProps = React.useRef<unknown>()
const lastWrapperProps = React.useRef(wrapperProps)
const childPropsFromStoreUpdate = React.useRef<unknown>()
const renderIsScheduled = React.useRef(false)
const isProcessingDispatch = React.useRef(false)
const isMounted = React.useRef(false)

const latestSubscriptionCallbackError = useRef<Error>()
const latestSubscriptionCallbackError = React.useRef<Error>()

useIsomorphicLayoutEffect(() => {
isMounted.current = true
Expand All @@ -648,7 +648,7 @@ function connect<
}
}, [])

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

const subscribeForReact = useMemo(() => {
const subscribeForReact = React.useMemo(() => {
const subscribe = (reactListener: () => void) => {
if (!subscription) {
return () => {}
Expand Down Expand Up @@ -741,7 +741,7 @@ function connect<

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

// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() => {
const renderedChild = React.useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
Expand Down
4 changes: 2 additions & 2 deletions src/next.ts
Expand Up @@ -3,7 +3,7 @@
// The useSyncExternalStoreWithSelector has to be imported, but we can use the
// non-shim version. This shaves off the byte size of the shim.

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

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

initializeUseSelector(useSyncExternalStoreWithSelector)
initializeConnect(useSyncExternalStore)
initializeConnect(React.useSyncExternalStore)

// Enable batched updates in our subscriptions for use
// with standard React renderers (ReactDOM, React Native)
Expand Down
4 changes: 2 additions & 2 deletions src/utils/useIsomorphicLayoutEffect.native.ts
@@ -1,5 +1,5 @@
import { useLayoutEffect } from 'react'
import * as React from 'react'

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

export const useIsomorphicLayoutEffect = useLayoutEffect
export const useIsomorphicLayoutEffect = React.useLayoutEffect
6 changes: 4 additions & 2 deletions src/utils/useIsomorphicLayoutEffect.ts
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect } from 'react'
import * as React from 'react'

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

export const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect
export const useIsomorphicLayoutEffect = canUseDOM
? React.useLayoutEffect
: React.useEffect

0 comments on commit 2ac527b

Please sign in to comment.