diff --git a/.babelrc.js b/.babelrc.js index 343797796..9f7a6d998 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -12,6 +12,7 @@ module.exports = { // Use the equivalent of `babel-preset-modules` bugfixes: true, modules: false, + loose: true, }, ], '@babel/preset-typescript', @@ -19,6 +20,9 @@ module.exports = { plugins: [ ['@babel/proposal-decorators', { legacy: true }], '@babel/transform-react-jsx', + ['@babel/plugin-proposal-class-properties', { loose: true }], + ['@babel/plugin-proposal-private-methods', { loose: true }], + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], cjs && ['@babel/transform-modules-commonjs'], [ '@babel/transform-runtime', diff --git a/src/components/Context.ts b/src/components/Context.ts index 04068d1c7..08705ece4 100644 --- a/src/components/Context.ts +++ b/src/components/Context.ts @@ -1,7 +1,7 @@ import React from 'react' import { Action, AnyAction, Store } from 'redux' import type { FixTypeLater } from '../types' -import type Subscription from '../utils/Subscription' +import type { Subscription } from '../utils/Subscription' export interface ReactReduxContextValue< SS = FixTypeLater, @@ -11,9 +11,8 @@ export interface ReactReduxContextValue< subscription: Subscription } -export const ReactReduxContext = /*#__PURE__*/ React.createContext( - null -) +export const ReactReduxContext = + /*#__PURE__*/ React.createContext(null) if (process.env.NODE_ENV !== 'production') { ReactReduxContext.displayName = 'ReactRedux' diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index bbd84d0a1..32f100994 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -1,6 +1,6 @@ import React, { Context, ReactNode, useMemo } from 'react' import { ReactReduxContext, ReactReduxContextValue } from './Context' -import Subscription from '../utils/Subscription' +import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' import type { FixTypeLater } from '../types' import { Action, AnyAction, Store } from 'redux' @@ -21,7 +21,7 @@ interface ProviderProps { function Provider({ store, context, children }: ProviderProps) { const contextValue = useMemo(() => { - const subscription = new Subscription(store) + const subscription = createSubscription(store) subscription.onStateChange = subscription.notifyNestedSubs return { store, diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 01f4a5553..191350dac 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -1,7 +1,7 @@ import hoistStatics from 'hoist-non-react-statics' import React, { useContext, useMemo, useRef, useReducer } from 'react' import { isValidElementType, isContextConsumer } from 'react-is' -import Subscription from '../utils/Subscription' +import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' import { ReactReduxContext } from './Context' @@ -334,7 +334,7 @@ export default function connectAdvanced( // This Subscription's source should match where store came from: props vs. context. A component // connected to the store via props shouldn't use subscription from context, or vice versa. - const subscription = new Subscription( + const subscription = createSubscription( store, didStoreComeFromProps ? null : contextValue.subscription ) @@ -343,9 +343,8 @@ export default function connectAdvanced( // the middle of the notification loop, where `subscription` will then be null. This can // probably be avoided if Subscription's listeners logic is changed to not call listeners // that have been unsubscribed in the middle of the notification loop. - const notifyNestedSubs = subscription.notifyNestedSubs.bind( - subscription - ) + const notifyNestedSubs = + subscription.notifyNestedSubs.bind(subscription) return [subscription, notifyNestedSubs] }, [store, didStoreComeFromProps, contextValue]) diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index 6354873b2..946721edb 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -1,16 +1,16 @@ import { useReducer, useRef, useMemo, useContext, useDebugValue } from 'react' import { useReduxContext as useDefaultReduxContext } from './useReduxContext' -import Subscription from '../utils/Subscription' +import { createSubscription, Subscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' import { ReactReduxContext } from '../components/Context' import { AnyAction, Store } from 'redux' import { DefaultRootState } from '../types' -type EqualityFn = (a: T | undefined, b: T | undefined) => boolean; +type EqualityFn = (a: T | undefined, b: T | undefined) => boolean const refEquality: EqualityFn = (a, b) => a === b -type TSelector = (state: S) => R; +type TSelector = (state: S) => R function useSelectorWithStoreAndSubscription( selector: TSelector, @@ -20,10 +20,10 @@ function useSelectorWithStoreAndSubscription( ): TSelectedState { const [, forceRender] = useReducer((s) => s + 1, 0) - const subscription = useMemo(() => new Subscription(store, contextSub), [ - store, - contextSub, - ]) + const subscription = useMemo( + () => createSubscription(store, contextSub), + [store, contextSub] + ) const latestSubscriptionCallbackError = useRef() const latestSelector = useRef>() @@ -107,13 +107,21 @@ function useSelectorWithStoreAndSubscription( * @param {React.Context} [context=ReactReduxContext] Context passed to your ``. * @returns {Function} A `useSelector` hook bound to the specified context. */ -export function createSelectorHook(context = ReactReduxContext): (selector: (state: TState) => Selected, equalityFn?: EqualityFn) => Selected { +export function createSelectorHook( + context = ReactReduxContext +): ( + selector: (state: TState) => Selected, + equalityFn?: EqualityFn +) => Selected { const useReduxContext = context === ReactReduxContext ? useDefaultReduxContext : () => useContext(context) - - return function useSelector(selector: (state: TState) => Selected, equalityFn: EqualityFn = refEquality): Selected { + + return function useSelector( + selector: (state: TState) => Selected, + equalityFn: EqualityFn = refEquality + ): Selected { if (process.env.NODE_ENV !== 'production') { if (!selector) { throw new Error(`You must pass a selector to useSelector`) diff --git a/src/utils/Subscription.ts b/src/utils/Subscription.ts index d708e3b32..2abeef399 100644 --- a/src/utils/Subscription.ts +++ b/src/utils/Subscription.ts @@ -4,8 +4,10 @@ import { getBatch } from './batch' // well as nesting subscriptions of descendant components, so that we can ensure the // ancestor components re-render before descendants +type VoidFunc = () => void + type Listener = { - callback: () => void + callback: VoidFunc next: Listener | null prev: Listener | null } @@ -77,55 +79,73 @@ function createListenerCollection() { type ListenerCollection = ReturnType -export default class Subscription { - private store: any - private parentSub?: Subscription - private unsubscribe?: () => void - private listeners?: ListenerCollection - public onStateChange?: () => void +export interface Subscription { + addNestedSub: (listener: VoidFunc) => VoidFunc + notifyNestedSubs: VoidFunc + handleChangeWrapper: VoidFunc + isSubscribed: () => boolean + onStateChange?: VoidFunc + trySubscribe: VoidFunc + tryUnsubscribe: VoidFunc + getListeners: () => ListenerCollection +} - constructor(store: any, parentSub?: Subscription) { - this.store = store - this.parentSub = parentSub - this.unsubscribe = undefined - this.listeners = undefined +const nullListeners = { + notify() {}, + get: () => [], +} as unknown as ListenerCollection - this.handleChangeWrapper = this.handleChangeWrapper.bind(this) - } +export function createSubscription(store: any, parentSub?: Subscription) { + let unsubscribe: VoidFunc | undefined + let listeners: ListenerCollection = nullListeners - addNestedSub(listener: () => void) { - this.trySubscribe() - return this.listeners?.subscribe(listener) + function addNestedSub(listener: () => void) { + trySubscribe() + return listeners.subscribe(listener) } - notifyNestedSubs() { - this.listeners?.notify() + function notifyNestedSubs() { + listeners.notify() } - handleChangeWrapper() { - this.onStateChange?.() + function handleChangeWrapper() { + if (subscription.onStateChange) { + subscription.onStateChange() + } } - isSubscribed() { - return Boolean(this.unsubscribe) + function isSubscribed() { + return Boolean(unsubscribe) } - trySubscribe() { - if (!this.unsubscribe) { - this.unsubscribe = this.parentSub - ? this.parentSub.addNestedSub(this.handleChangeWrapper) - : this.store.subscribe(this.handleChangeWrapper) + function trySubscribe() { + if (!unsubscribe) { + unsubscribe = parentSub + ? parentSub.addNestedSub(handleChangeWrapper) + : store.subscribe(handleChangeWrapper) - this.listeners = createListenerCollection() + listeners = createListenerCollection() } } - tryUnsubscribe() { - if (this.unsubscribe) { - this.unsubscribe() - this.unsubscribe = undefined - this.listeners?.clear() - this.listeners = undefined + function tryUnsubscribe() { + if (unsubscribe) { + unsubscribe() + unsubscribe = undefined + listeners.clear() + listeners = nullListeners } } + + const subscription: Subscription = { + addNestedSub, + notifyNestedSubs, + handleChangeWrapper, + isSubscribed, + trySubscribe, + tryUnsubscribe, + getListeners: () => listeners, + } + + return subscription } diff --git a/src/utils/bindActionCreators.ts b/src/utils/bindActionCreators.ts index 3ab75f823..1e0fa9a34 100644 --- a/src/utils/bindActionCreators.ts +++ b/src/utils/bindActionCreators.ts @@ -1,28 +1,10 @@ -import { - ActionCreator, - ActionCreatorsMapObject, - AnyAction, - Dispatch, -} from 'redux' - -function bindActionCreator( - actionCreator: ActionCreator, - dispatch: Dispatch -) { - return function (this: any, ...args: any[]) { - return dispatch(actionCreator.apply(this, args)) - } -} +import { ActionCreatorsMapObject, Dispatch } from 'redux' export default function bindActionCreators( - actionCreators: ActionCreator | ActionCreatorsMapObject, + actionCreators: ActionCreatorsMapObject, dispatch: Dispatch ) { - if (typeof actionCreators === 'function') { - return bindActionCreator(actionCreators, dispatch) - } - - const boundActionCreators: ActionCreatorsMapObject = {} + const boundActionCreators: ActionCreatorsMapObject = {} for (const key in actionCreators) { const actionCreator = actionCreators[key] if (typeof actionCreator === 'function') { diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index 17d7a80aa..2f927214e 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -101,11 +101,11 @@ describe('React', () => { ) - expect(rootSubscription.listeners.get().length).toBe(1) + expect(rootSubscription.getListeners().get().length).toBe(1) store.dispatch({ type: '' }) - expect(rootSubscription.listeners.get().length).toBe(2) + expect(rootSubscription.getListeners().get().length).toBe(2) }) it('unsubscribes when the component is unmounted', () => { @@ -129,11 +129,11 @@ describe('React', () => { ) - expect(rootSubscription.listeners.get().length).toBe(2) + expect(rootSubscription.getListeners().get().length).toBe(2) store.dispatch({ type: '' }) - expect(rootSubscription.listeners.get().length).toBe(1) + expect(rootSubscription.getListeners().get().length).toBe(1) }) it('notices store updates between render and store subscription effect', () => { diff --git a/test/utils/Subscription.spec.js b/test/utils/Subscription.spec.js index 417c81b22..0cf0d4328 100644 --- a/test/utils/Subscription.spec.js +++ b/test/utils/Subscription.spec.js @@ -1,4 +1,4 @@ -import Subscription from '../../src/utils/Subscription' +import { createSubscription } from '../../src/utils/Subscription' describe('Subscription', () => { let notifications @@ -9,13 +9,13 @@ describe('Subscription', () => { notifications = [] store = { subscribe: () => jest.fn() } - parent = new Subscription(store) + parent = createSubscription(store) parent.onStateChange = () => {} parent.trySubscribe() }) function subscribeChild(name) { - const child = new Subscription(store, parent) + const child = createSubscription(store, parent) child.onStateChange = () => notifications.push(name) child.trySubscribe() return child