Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSC-specific workarounds #2050

Merged
merged 4 commits into from Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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

Copy link
Member Author

@phryneas phryneas Jul 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an environment where createContext does not exist we can pretty much just return anything - there is no useContext that could consume it anyways - and this avoids a hard error being thrown futher down.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and the provider needs to be in a client component, so presumably that won't be an issue either

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