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

Improve treeshakeability of build artifacts #2176

Merged
merged 26 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f81f91
Fix `tsup` config
aryaemami59 May 31, 2024
157e6c9
Bring all of `devModeChecks` underneath `process.env.NODE_ENV` checks
aryaemami59 May 31, 2024
fd88816
Add `getUseIsomorphicLayoutEffect` with `@__PURE__` annotation
aryaemami59 May 31, 2024
b14e12a
Add `@__PURE__` to `ContextKey` creation site
aryaemami59 May 31, 2024
4e2479b
Add `@__PURE__` annotations to all `react-is` symbols
aryaemami59 May 31, 2024
c838d4c
Convert `isReactNative` and `canUseDOM` to functions with `@__PURE__`s
aryaemami59 Jun 1, 2024
3d2f07f
Do not export `isRunningInReactNative` as it's usage is internal
aryaemami59 Jun 1, 2024
02de09a
Replace `define` with `env` as it is the same thing.
aryaemami59 Jun 4, 2024
2cc0eef
Fix `valid-jsdoc` rule options to not require parameter or return types
aryaemami59 Jun 10, 2024
35858b1
Emit type definitions with `cjs` format
aryaemami59 Jun 10, 2024
853c462
Fix `React` namespace import
aryaemami59 Jun 10, 2024
fc63d04
Add `src` to `files`
aryaemami59 Jun 25, 2024
d0f5a9c
Remove unused `types\index.d.ts` file
aryaemami59 Jun 25, 2024
b07d821
Remove `initializeConnect` and `initializeUseSelector`
aryaemami59 Jun 25, 2024
61e1950
Remove duplicate `default` export of `ReactReduxContext`
aryaemami59 Jun 26, 2024
fe6eba1
Undo unused export of `getDependsOnOwnProps`
aryaemami59 Jun 26, 2024
8d939ba
Undo unused export of `pureFinalPropsSelectorFactory`
aryaemami59 Jun 26, 2024
198253e
Undo unused export of `defaultMergeProps`
aryaemami59 Jun 26, 2024
a1b526c
Undo unused export of `wrapMergePropsFunc`
aryaemami59 Jun 26, 2024
278d61a
Replace `prepare` script with `prepack`
aryaemami59 Aug 6, 2024
2fc58b1
Fix imports in `connect.tsx`
aryaemami59 Aug 13, 2024
146dd92
Fix imports in `Context.ts`
aryaemami59 Aug 13, 2024
f7fa960
Fix imports in `mergeProps.ts`
aryaemami59 Aug 13, 2024
912bdce
Add JSDoc `@returns` tag for `isRunningInReactNative`
aryaemami59 Sep 17, 2024
5f64c8b
Bring dev mode checks underneath `process.env.NODE_ENV` references
aryaemami59 Oct 11, 2024
49f8136
Bump `tsup` to version 8.3.5
aryaemami59 Jul 17, 2024
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
10 changes: 8 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
},
"plugins": ["@typescript-eslint", "import", "react"],
"rules": {
"valid-jsdoc": [2],
"valid-jsdoc": [
2,
{ "requireReturnType": false, "requireParamType": false }
],
"react/no-is-mounted": [0]
},
"settings": {
Expand Down Expand Up @@ -55,7 +58,10 @@
{ "fixStyle": "separate-type-imports" }
],
"@typescript-eslint/consistent-type-exports": [2],
"valid-jsdoc": [2],
"valid-jsdoc": [
2,
{ "requireReturnType": false, "requireParamType": false }
],
"react/no-is-mounted": [0]
}
},
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
},
"sideEffects": false,
"files": [
"dist"
"dist",
"src"
],
"scripts": {
"build": "yarn clean && tsup",
Expand All @@ -42,7 +43,7 @@
"format": "prettier --write \"{src,test}/**/*.{js,ts,tsx}\" \"docs/**/*.md\"",
"lint": "eslint src test",
"lint:fix": "eslint src test --fix",
"prepare": "yarn clean && yarn build",
"prepack": "yarn build",
"pretest": "yarn lint",
"test": "vitest --run --typecheck",
"test:watch": "vitest --watch",
Expand Down Expand Up @@ -102,7 +103,7 @@
"react-test-renderer": "18.3.1",
"redux": "^5.0.1",
"rimraf": "^5.0.7",
"tsup": "7.0.0",
"tsup": "^8.3.5",
"typescript": "^5.5.4",
"typescript-eslint": "^7.12.0",
"vitest": "^1.6.0"
Expand Down
4 changes: 1 addition & 3 deletions src/components/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface ReactReduxContextValue<
getServerState?: () => SS
}

const ContextKey = Symbol.for(`react-redux-context`)
const ContextKey = /* @__PURE__ */ Symbol.for(`react-redux-context`)
const gT: {
[ContextKey]?: Map<
typeof React.createContext,
Expand Down Expand Up @@ -48,5 +48,3 @@ function getContext(): Context<ReactReduxContextValue | null> {
export const ReactReduxContext = /*#__PURE__*/ getContext()

export type ReactReduxContextInstance = typeof ReactReduxContext

export default ReactReduxContext
32 changes: 20 additions & 12 deletions src/components/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,32 @@ export interface ProviderProps<
children: ReactNode
}

function Provider<A extends Action<string> = UnknownAction, S = unknown>({
store,
context,
children,
serverState,
stabilityCheck = 'once',
identityFunctionCheck = 'once',
}: ProviderProps<A, S>) {
function Provider<A extends Action<string> = UnknownAction, S = unknown>(
providerProps: ProviderProps<A, S>,
) {
const { children, context, serverState, store } = providerProps

const contextValue = React.useMemo(() => {
const subscription = createSubscription(store)
return {

const baseContextValue = {
store,
subscription,
getServerState: serverState ? () => serverState : undefined,
stabilityCheck,
identityFunctionCheck,
}
}, [store, serverState, stabilityCheck, identityFunctionCheck])

if (process.env.NODE_ENV === 'production') {
return baseContextValue
} else {
const { identityFunctionCheck = 'once', stabilityCheck = 'once' } =
providerProps

return /* @__PURE__ */ Object.assign(baseContextValue, {
stabilityCheck,
identityFunctionCheck,
})
}
}, [store, serverState])

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

Expand Down
10 changes: 1 addition & 9 deletions src/components/connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ import type {
} from './Context'
import { ReactReduxContext } from './Context'

import type { uSES } from '../utils/useSyncExternalStore'
import { notInitialized } from '../utils/useSyncExternalStore'

let useSyncExternalStore = notInitialized as uSES
export const initializeConnect = (fn: uSES) => {
useSyncExternalStore = fn
}

// Define some constant arrays just to avoid re-creating these
const EMPTY_ARRAY: [unknown, number] = [null, 0]
const NO_SUBSCRIPTION_ARRAY = [null, null]
Expand Down Expand Up @@ -726,7 +718,7 @@ function connect<
let actualChildProps: Record<string, unknown>

try {
actualChildProps = useSyncExternalStore(
actualChildProps = React.useSyncExternalStore(
// TODO We're passing through a big wrapper that does a bunch of extra side effects besides subscribing
subscribeForReact,
// TODO This is incredibly hacky. We've already processed the store update and calculated new child props,
Expand Down
4 changes: 2 additions & 2 deletions src/connect/mergeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createInvalidArgFactory } from './invalidArgFactory'
import type { MergeProps } from './selectorFactory'
import type { EqualityFn } from '../types'

export function defaultMergeProps<
function defaultMergeProps<
TStateProps,
TDispatchProps,
TOwnProps,
Expand All @@ -18,7 +18,7 @@ export function defaultMergeProps<
return { ...ownProps, ...stateProps, ...dispatchProps }
}

export function wrapMergePropsFunc<
function wrapMergePropsFunc<
TStateProps,
TDispatchProps,
TOwnProps,
Expand Down
2 changes: 1 addition & 1 deletion src/connect/selectorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ interface PureSelectorFactoryComparisonOptions<TStateProps, TOwnProps, State> {
readonly areOwnPropsEqual: EqualityFn<TOwnProps>
}

export function pureFinalPropsSelectorFactory<
function pureFinalPropsSelectorFactory<
TStateProps,
TOwnProps,
TDispatchProps,
Expand Down
2 changes: 1 addition & 1 deletion src/connect/wrapMapToProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function wrapMapToPropsConstant(
// A length of zero is assumed to mean mapToProps is getting args via arguments or ...args and
// therefore not reporting its length accurately..
// TODO Can this get pulled out so that we can subscribe directly to the store if we don't need ownProps?
export function getDependsOnOwnProps(mapToProps: MapToProps) {
function getDependsOnOwnProps(mapToProps: MapToProps) {
return mapToProps.dependsOnOwnProps
? Boolean(mapToProps.dependsOnOwnProps)
: mapToProps.length !== 1
Expand Down
28 changes: 11 additions & 17 deletions src/hooks/useSelector.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
//import * as React from 'react'
import { React } from '../utils/react'

import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector.js'
import type { ReactReduxContextValue } from '../components/Context'
import { ReactReduxContext } from '../components/Context'
import type { EqualityFn, NoInfer } from '../types'
import type { uSESWS } from '../utils/useSyncExternalStore'
import { notInitialized } from '../utils/useSyncExternalStore'
import {
createReduxContextHook,
useReduxContext as useDefaultReduxContext,
Expand Down Expand Up @@ -118,11 +116,6 @@ export interface UseSelector<StateType = unknown> {
>() => UseSelector<OverrideStateType>
}

let useSyncExternalStoreWithSelector = notInitialized as uSESWS
export const initializeUseSelector = (fn: uSESWS) => {
useSyncExternalStoreWithSelector = fn
}

const refEquality: EqualityFn<any> = (a, b) => a === b

/**
Expand All @@ -148,7 +141,7 @@ export function createSelectorHook(
| EqualityFn<NoInfer<Selected>>
| UseSelectorOptions<NoInfer<Selected>> = {},
): Selected => {
const { equalityFn = refEquality, devModeChecks = {} } =
const { equalityFn = refEquality } =
typeof equalityFnOrOptions === 'function'
? { equalityFn: equalityFnOrOptions }
: equalityFnOrOptions
Expand All @@ -166,13 +159,9 @@ export function createSelectorHook(
}
}

const {
store,
subscription,
getServerState,
stabilityCheck,
identityFunctionCheck,
} = useReduxContext()
const reduxContext = useReduxContext()

const { store, subscription, getServerState } = reduxContext

const firstRun = React.useRef(true)

Expand All @@ -181,6 +170,11 @@ export function createSelectorHook(
[selector.name](state: TState) {
const selected = selector(state)
if (process.env.NODE_ENV !== 'production') {
const { devModeChecks = {} } =
typeof equalityFnOrOptions === 'function'
? {}
: equalityFnOrOptions
const { identityFunctionCheck, stabilityCheck } = reduxContext
const {
identityFunctionCheck: finalIdentityFunctionCheck,
stabilityCheck: finalStabilityCheck,
Expand Down Expand Up @@ -243,7 +237,7 @@ export function createSelectorHook(
return selected
},
}[selector.name],
[selector, stabilityCheck, devModeChecks.stabilityCheck],
[selector],
)

const selectedState = useSyncExternalStoreWithSelector(
Expand Down
14 changes: 0 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1 @@
// The primary entry point assumes we are working with React 18, and thus have
// useSyncExternalStore available. We can import that directly from React itself.
// The useSyncExternalStoreWithSelector has to be imported, but we can use the
// non-shim version. This shaves off the byte size of the shim.

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

import { initializeUseSelector } from './hooks/useSelector'
import { initializeConnect } from './components/connect'

initializeUseSelector(useSyncExternalStoreWithSelector)
initializeConnect(React.useSyncExternalStore)

export * from './exports'
36 changes: 21 additions & 15 deletions src/utils/react-is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import type { ElementType, MemoExoticComponent, ReactElement } from 'react'
// It's very possible this could change in the future, but given that
// we only use these in `connect`, this is a low priority.

const REACT_ELEMENT_TYPE = Symbol.for('react.element')
const REACT_PORTAL_TYPE = Symbol.for('react.portal')
const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment')
const REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode')
const REACT_PROFILER_TYPE = Symbol.for('react.profiler')
const REACT_PROVIDER_TYPE = Symbol.for('react.provider')
const REACT_CONTEXT_TYPE = Symbol.for('react.context')
const REACT_SERVER_CONTEXT_TYPE = Symbol.for('react.server_context')
const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref')
const REACT_SUSPENSE_TYPE = Symbol.for('react.suspense')
const REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list')
const REACT_MEMO_TYPE = Symbol.for('react.memo')
const REACT_LAZY_TYPE = Symbol.for('react.lazy')
const REACT_OFFSCREEN_TYPE = Symbol.for('react.offscreen')
const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference')
const REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for('react.element')
const REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for('react.portal')
const REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for('react.fragment')
const REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for('react.strict_mode')
const REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for('react.profiler')
const REACT_PROVIDER_TYPE = /* @__PURE__ */ Symbol.for('react.provider')
const REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for('react.context')
const REACT_SERVER_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for(
'react.server_context',
)
const REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for('react.forward_ref')
const REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for('react.suspense')
const REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for(
'react.suspense_list',
)
const REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for('react.memo')
const REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for('react.lazy')
const REACT_OFFSCREEN_TYPE = /* @__PURE__ */ Symbol.for('react.offscreen')
const REACT_CLIENT_REFERENCE = /* @__PURE__ */ Symbol.for(
'react.client.reference',
)

export const ForwardRef = REACT_FORWARD_REF_TYPE
export const Memo = REACT_MEMO_TYPE
Expand Down
7 changes: 2 additions & 5 deletions src/utils/react.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import type * as ReactNamespace from 'react'
import * as ReactOriginal from 'react'
import * as React from 'react'

export const React: typeof ReactNamespace =
// prettier-ignore
'default' in ReactOriginal ? ReactOriginal['default'] : ReactOriginal as any
export { React }
24 changes: 17 additions & 7 deletions src/utils/useIsomorphicLayoutEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,31 @@ import { React } from '../utils/react'
// subscription is created and an inconsistent state may be observed

// Matches logic in React's `shared/ExecutionEnvironment` file
export const canUseDOM = !!(
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
)
const canUseDOM = () =>
!!(
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
)

const isDOM = /* @__PURE__ */ canUseDOM()

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

/**
* Checks if the code is running in a React Native environment.
*
* @returns Whether the code is running in a React Native environment.
*
* @see {@link https://github.com/facebook/react-native/issues/1331 Reference}
*/
export const isReactNative =
const isRunningInReactNative = () =>
typeof navigator !== 'undefined' && navigator.product === 'ReactNative'

const isReactNative = /* @__PURE__ */ isRunningInReactNative()

const getUseIsomorphicLayoutEffect = () =>
isDOM || isReactNative ? React.useLayoutEffect : React.useEffect

export const useIsomorphicLayoutEffect =
canUseDOM || isReactNative ? React.useLayoutEffect : React.useEffect
/* @__PURE__ */ getUseIsomorphicLayoutEffect()
Loading
Loading