Skip to content

Commit b8a118a

Browse files
authored
Convert connect components and logic to TS (with caveats) (#1758)
1 parent 3a243ff commit b8a118a

File tree

9 files changed

+535
-400
lines changed

9 files changed

+535
-400
lines changed

src/components/Context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface ReactReduxContextValue<
1414
export const ReactReduxContext =
1515
/*#__PURE__*/ React.createContext<ReactReduxContextValue | null>(null)
1616

17+
export type ReactReduxContextInstance = typeof ReactReduxContext
18+
1719
if (process.env.NODE_ENV !== 'production') {
1820
ReactReduxContext.displayName = 'ReactRedux'
1921
}

src/components/connectAdvanced.js renamed to src/components/connectAdvanced.tsx

Lines changed: 112 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,61 @@
11
import hoistStatics from 'hoist-non-react-statics'
2-
import React, { useContext, useMemo, useRef, useReducer } from 'react'
2+
import React, {
3+
useContext,
4+
useMemo,
5+
useRef,
6+
useReducer,
7+
useLayoutEffect,
8+
} from 'react'
39
import { isValidElementType, isContextConsumer } from 'react-is'
4-
import { createSubscription } from '../utils/Subscription'
10+
import type { Store } from 'redux'
11+
import type { SelectorFactory } from '../connect/selectorFactory'
12+
import { createSubscription, Subscription } from '../utils/Subscription'
513
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
614

7-
import { ReactReduxContext } from './Context'
15+
import {
16+
ReactReduxContext,
17+
ReactReduxContextValue,
18+
ReactReduxContextInstance,
19+
} from './Context'
820

921
// Define some constant arrays just to avoid re-creating these
10-
const EMPTY_ARRAY = []
22+
const EMPTY_ARRAY: [unknown, number] = [null, 0]
1123
const NO_SUBSCRIPTION_ARRAY = [null, null]
1224

13-
const stringifyComponent = (Comp) => {
25+
const stringifyComponent = (Comp: unknown) => {
1426
try {
1527
return JSON.stringify(Comp)
1628
} catch (err) {
1729
return String(Comp)
1830
}
1931
}
2032

21-
function storeStateUpdatesReducer(state, action) {
33+
function storeStateUpdatesReducer(
34+
state: [payload: unknown, counter: number],
35+
action: { payload: unknown }
36+
) {
2237
const [, updateCount] = state
2338
return [action.payload, updateCount + 1]
2439
}
2540

41+
type EffectFunc = (...args: any[]) => void | ReturnType<React.EffectCallback>
42+
2643
function useIsomorphicLayoutEffectWithArgs(
27-
effectFunc,
28-
effectArgs,
29-
dependencies
44+
effectFunc: EffectFunc,
45+
effectArgs: any[],
46+
dependencies?: React.DependencyList
3047
) {
3148
useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies)
3249
}
3350

3451
function captureWrapperProps(
35-
lastWrapperProps,
36-
lastChildProps,
37-
renderIsScheduled,
38-
wrapperProps,
39-
actualChildProps,
40-
childPropsFromStoreUpdate,
41-
notifyNestedSubs
52+
lastWrapperProps: React.MutableRefObject<unknown>,
53+
lastChildProps: React.MutableRefObject<unknown>,
54+
renderIsScheduled: React.MutableRefObject<boolean>,
55+
wrapperProps: React.MutableRefObject<unknown>,
56+
actualChildProps: React.MutableRefObject<unknown>,
57+
childPropsFromStoreUpdate: React.MutableRefObject<unknown>,
58+
notifyNestedSubs: () => void
4259
) {
4360
// We want to capture the wrapper props and child props we used for later comparisons
4461
lastWrapperProps.current = wrapperProps
@@ -53,23 +70,23 @@ function captureWrapperProps(
5370
}
5471

5572
function subscribeUpdates(
56-
shouldHandleStateChanges,
57-
store,
58-
subscription,
59-
childPropsSelector,
60-
lastWrapperProps,
61-
lastChildProps,
62-
renderIsScheduled,
63-
childPropsFromStoreUpdate,
64-
notifyNestedSubs,
65-
forceComponentUpdateDispatch
73+
shouldHandleStateChanges: boolean,
74+
store: Store,
75+
subscription: Subscription,
76+
childPropsSelector: (state: unknown, props: unknown) => unknown,
77+
lastWrapperProps: React.MutableRefObject<unknown>,
78+
lastChildProps: React.MutableRefObject<unknown>,
79+
renderIsScheduled: React.MutableRefObject<boolean>,
80+
childPropsFromStoreUpdate: React.MutableRefObject<unknown>,
81+
notifyNestedSubs: () => void,
82+
forceComponentUpdateDispatch: React.Dispatch<any>
6683
) {
6784
// If we're not subscribed to the store, nothing to do here
6885
if (!shouldHandleStateChanges) return
6986

7087
// Capture values for checking if and when this component unmounts
7188
let didUnsubscribe = false
72-
let lastThrownError = null
89+
let lastThrownError: Error | null = null
7390

7491
// We'll run this callback every time a store subscription update propagates to this component
7592
const checkForUpdates = () => {
@@ -148,7 +165,29 @@ function subscribeUpdates(
148165
return unsubscribeWrapper
149166
}
150167

151-
const initStateUpdates = () => [null, 0]
168+
const initStateUpdates = () => EMPTY_ARRAY
169+
170+
export interface ConnectProps {
171+
reactReduxForwardedRef?: React.ForwardedRef<unknown>
172+
context?: ReactReduxContextInstance
173+
store?: Store
174+
}
175+
176+
export type ConnectedComponent<
177+
C extends React.ComponentType<any>,
178+
P
179+
> = React.NamedExoticComponent<JSX.LibraryManagedAttributes<C, P>> & {
180+
WrappedComponent: C
181+
}
182+
183+
interface ConnectAdvancedOptions {
184+
getDisplayName?: (name: string) => string
185+
methodName?: string
186+
shouldHandleStateChanges?: boolean
187+
forwardRef?: boolean
188+
context?: typeof ReactReduxContext
189+
pure?: boolean
190+
}
152191

153192
export default function connectAdvanced(
154193
/*
@@ -168,7 +207,7 @@ export default function connectAdvanced(
168207
props. Do not use connectAdvanced directly without memoizing results between calls to your
169208
selector, otherwise the Connect component will re-render on every state or props change.
170209
*/
171-
selectorFactory,
210+
selectorFactory: SelectorFactory<unknown, unknown, unknown, unknown>,
172211
// options object:
173212
{
174213
// the func used to compute this HOC's displayName from the wrapped component's displayName.
@@ -179,19 +218,9 @@ export default function connectAdvanced(
179218
// probably overridden by wrapper functions such as connect()
180219
methodName = 'connectAdvanced',
181220

182-
// REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
183-
// calls to render. useful for watching in react devtools for unnecessary re-renders.
184-
renderCountProp = undefined,
185-
186221
// determines whether this HOC subscribes to store changes
187222
shouldHandleStateChanges = true,
188223

189-
// REMOVED: the key of props/context to get the store
190-
storeKey = 'store',
191-
192-
// REMOVED: expose the wrapped component via refs
193-
withRef = false,
194-
195224
// use React's forwardRef to expose a ref of the wrapped component
196225
forwardRef = false,
197226

@@ -200,37 +229,13 @@ export default function connectAdvanced(
200229

201230
// additional options are passed through to the selectorFactory
202231
...connectOptions
203-
} = {}
232+
}: ConnectAdvancedOptions = {}
204233
) {
205-
if (process.env.NODE_ENV !== 'production') {
206-
if (renderCountProp !== undefined) {
207-
throw new Error(
208-
`renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension`
209-
)
210-
}
211-
if (withRef) {
212-
throw new Error(
213-
'withRef is removed. To access the wrapped instance, use a ref on the connected component'
214-
)
215-
}
216-
217-
const customStoreWarningMessage =
218-
'To use a custom Redux store for specific components, create a custom React context with ' +
219-
"React.createContext(), and pass the context object to React Redux's Provider and specific components" +
220-
' like: <Provider context={MyContext}><ConnectedComponent context={MyContext} /></Provider>. ' +
221-
'You may also pass a {context : MyContext} option to connect'
222-
223-
if (storeKey !== 'store') {
224-
throw new Error(
225-
'storeKey has been removed and does not do anything. ' +
226-
customStoreWarningMessage
227-
)
228-
}
229-
}
230-
231234
const Context = context
232235

233-
return function wrapWithConnect(WrappedComponent) {
236+
return function wrapWithConnect<WC extends React.ComponentType>(
237+
WrappedComponent: WC
238+
) {
234239
if (
235240
process.env.NODE_ENV !== 'production' &&
236241
!isValidElementType(WrappedComponent)
@@ -252,43 +257,41 @@ export default function connectAdvanced(
252257
...connectOptions,
253258
getDisplayName,
254259
methodName,
255-
renderCountProp,
256260
shouldHandleStateChanges,
257-
storeKey,
258261
displayName,
259262
wrappedComponentName,
260263
WrappedComponent,
261264
}
262265

263266
const { pure } = connectOptions
264267

265-
function createChildSelector(store) {
268+
function createChildSelector(store: Store) {
266269
return selectorFactory(store.dispatch, selectorFactoryOptions)
267270
}
268271

269272
// If we aren't running in "pure" mode, we don't want to memoize values.
270273
// To avoid conditionally calling hooks, we fall back to a tiny wrapper
271274
// that just executes the given callback immediately.
272-
const usePureOnlyMemo = pure ? useMemo : (callback) => callback()
273-
274-
function ConnectFunction(props) {
275-
const [
276-
propsContext,
277-
reactReduxForwardedRef,
278-
wrapperProps,
279-
] = useMemo(() => {
280-
// Distinguish between actual "data" props that were passed to the wrapper component,
281-
// and values needed to control behavior (forwarded refs, alternate context instances).
282-
// To maintain the wrapperProps object reference, memoize this destructuring.
283-
const { reactReduxForwardedRef, ...wrapperProps } = props
284-
return [props.context, reactReduxForwardedRef, wrapperProps]
285-
}, [props])
286-
287-
const ContextToUse = useMemo(() => {
275+
const usePureOnlyMemo = pure
276+
? useMemo
277+
: (callback: () => void) => callback()
278+
279+
function ConnectFunction<TOwnProps>(props: ConnectProps & TOwnProps) {
280+
const [propsContext, reactReduxForwardedRef, wrapperProps] =
281+
useMemo(() => {
282+
// Distinguish between actual "data" props that were passed to the wrapper component,
283+
// and values needed to control behavior (forwarded refs, alternate context instances).
284+
// To maintain the wrapperProps object reference, memoize this destructuring.
285+
const { reactReduxForwardedRef, ...wrapperProps } = props
286+
return [props.context, reactReduxForwardedRef, wrapperProps]
287+
}, [props])
288+
289+
const ContextToUse: ReactReduxContextInstance = useMemo(() => {
288290
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
289291
// Memoize the check that determines which context instance we should use.
290292
return propsContext &&
291293
propsContext.Consumer &&
294+
// @ts-ignore
292295
isContextConsumer(<propsContext.Consumer />)
293296
? propsContext
294297
: Context
@@ -302,10 +305,10 @@ export default function connectAdvanced(
302305
// This allows us to pass through a `store` prop that is just a plain value.
303306
const didStoreComeFromProps =
304307
Boolean(props.store) &&
305-
Boolean(props.store.getState) &&
306-
Boolean(props.store.dispatch)
308+
Boolean(props.store!.getState) &&
309+
Boolean(props.store!.dispatch)
307310
const didStoreComeFromContext =
308-
Boolean(contextValue) && Boolean(contextValue.store)
311+
Boolean(contextValue) && Boolean(contextValue!.store)
309312

310313
if (
311314
process.env.NODE_ENV !== 'production' &&
@@ -321,7 +324,9 @@ export default function connectAdvanced(
321324
}
322325

323326
// Based on the previous check, one of these must be true
324-
const store = didStoreComeFromProps ? props.store : contextValue.store
327+
const store: Store = didStoreComeFromProps
328+
? props.store!
329+
: contextValue!.store
325330

326331
const childPropsSelector = useMemo(() => {
327332
// The child props selector needs the store reference as an input.
@@ -336,7 +341,7 @@ export default function connectAdvanced(
336341
// connected to the store via props shouldn't use subscription from context, or vice versa.
337342
const subscription = createSubscription(
338343
store,
339-
didStoreComeFromProps ? null : contextValue.subscription
344+
didStoreComeFromProps ? undefined : contextValue!.subscription
340345
)
341346

342347
// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
@@ -356,23 +361,26 @@ export default function connectAdvanced(
356361
// This component is directly subscribed to a store from props.
357362
// We don't want descendants reading from this store - pass down whatever
358363
// the existing context value is from the nearest connected ancestor.
359-
return contextValue
364+
return contextValue!
360365
}
361366

362367
// Otherwise, put this component's subscription instance into context, so that
363368
// connected descendants won't update until after this component is done
364369
return {
365370
...contextValue,
366371
subscription,
367-
}
372+
} as ReactReduxContextValue
368373
}, [didStoreComeFromProps, contextValue, subscription])
369374

370375
// We need to force this wrapper component to re-render whenever a Redux store update
371376
// causes a change to the calculated child component props (or we caught an error in mapState)
372-
const [
373-
[previousStateUpdateResult],
374-
forceComponentUpdateDispatch,
375-
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
377+
const [[previousStateUpdateResult], forceComponentUpdateDispatch] =
378+
useReducer(
379+
storeStateUpdatesReducer,
380+
// @ts-ignore
381+
EMPTY_ARRAY as any,
382+
initStateUpdates
383+
)
376384

377385
// Propagate any mapState/mapDispatch errors upwards
378386
if (previousStateUpdateResult && previousStateUpdateResult.error) {
@@ -441,6 +449,7 @@ export default function connectAdvanced(
441449
// We memoize the elements for the rendered child component as an optimization.
442450
const renderedWrappedComponent = useMemo(
443451
() => (
452+
// @ts-ignore
444453
<WrappedComponent
445454
{...actualChildProps}
446455
ref={reactReduxForwardedRef}
@@ -470,19 +479,23 @@ export default function connectAdvanced(
470479
}
471480

472481
// If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
473-
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
482+
const _Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
474483

484+
const Connect = _Connect as typeof _Connect & { WrappedComponent: WC }
475485
Connect.WrappedComponent = WrappedComponent
476486
Connect.displayName = ConnectFunction.displayName = displayName
477487

478488
if (forwardRef) {
479-
const forwarded = React.forwardRef(function forwardConnectRef(
489+
const _forwarded = React.forwardRef(function forwardConnectRef(
480490
props,
481491
ref
482492
) {
483493
return <Connect {...props} reactReduxForwardedRef={ref} />
484494
})
485495

496+
const forwarded = _forwarded as typeof _forwarded & {
497+
WrappedComponent: WC
498+
}
486499
forwarded.displayName = displayName
487500
forwarded.WrappedComponent = WrappedComponent
488501
return hoistStatics(forwarded, WrappedComponent)

0 commit comments

Comments
 (0)