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 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions src/components/Context.ts
Original file line number Diff line number Diff line change
@@ -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,30 @@ 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 = globalThis as {
[ContextKey]?: Map<
typeof React['createContext'],
Context<ReactReduxContextValue>
>
}

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


function getContext() {
let realContext = gT[ContextKey]
const contextMap = (gT[ContextKey] ??= new Map())
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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