diff --git a/src/utils/Subscription.ts b/src/utils/Subscription.ts index 76290be4a..a0431225d 100644 --- a/src/utils/Subscription.ts +++ b/src/utils/Subscription.ts @@ -99,9 +99,26 @@ export function createSubscription(store: any, parentSub?: Subscription) { let unsubscribe: VoidFunc | undefined let listeners: ListenerCollection = nullListeners + // Reasons to keep the subscription active + let subscriptionsAmount = 0 + + // Is this specific subscription subscribed (or only nested ones?) + let selfSubscribed = false + function addNestedSub(listener: () => void) { trySubscribe() - return listeners.subscribe(listener) + + const cleanupListener = listeners.subscribe(listener) + + // cleanup nested sub + let removed = false + return () => { + if (!removed) { + removed = true + cleanupListener() + tryUnsubscribe() + } + } } function notifyNestedSubs() { @@ -115,10 +132,11 @@ export function createSubscription(store: any, parentSub?: Subscription) { } function isSubscribed() { - return Boolean(unsubscribe) + return selfSubscribed } function trySubscribe() { + subscriptionsAmount++ if (!unsubscribe) { unsubscribe = parentSub ? parentSub.addNestedSub(handleChangeWrapper) @@ -129,7 +147,8 @@ export function createSubscription(store: any, parentSub?: Subscription) { } function tryUnsubscribe() { - if (unsubscribe) { + subscriptionsAmount-- + if (unsubscribe && subscriptionsAmount === 0) { unsubscribe() unsubscribe = undefined listeners.clear() @@ -137,13 +156,27 @@ export function createSubscription(store: any, parentSub?: Subscription) { } } + function trySubscribeSelf() { + if (!selfSubscribed) { + selfSubscribed = true + trySubscribe() + } + } + + function tryUnsubscribeSelf() { + if (selfSubscribed) { + selfSubscribed = false + tryUnsubscribe() + } + } + const subscription: Subscription = { addNestedSub, notifyNestedSubs, handleChangeWrapper, isSubscribed, - trySubscribe, - tryUnsubscribe, + trySubscribe: trySubscribeSelf, + tryUnsubscribe: tryUnsubscribeSelf, getListeners: () => listeners, } diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index 4da35efb8..c560f6925 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -6,6 +6,8 @@ import React, { useLayoutEffect, useState, useContext, + Suspense, + useEffect, } from 'react' import { createStore } from 'redux' import * as rtl from '@testing-library/react' @@ -723,6 +725,130 @@ describe('React', () => { const expectedMaxUnmountTime = IS_REACT_18 ? 500 : 7000 expect(elapsedTime).toBeLessThan(expectedMaxUnmountTime) }) + + it('keeps working when used inside a Suspense', async () => { + let result: number | undefined + let expectedResult: number | undefined + let lazyComponentAdded = false + let lazyComponentLoaded = false + + // A lazy loaded component in the Suspense + // This component does nothing really. It is lazy loaded to trigger the issue + // Lazy loading this component will break other useSelectors in the same Suspense + // See issue https://github.com/reduxjs/react-redux/issues/1977 + const OtherComp = () => { + useLayoutEffect(() => { + lazyComponentLoaded = true + }, []) + + return
+ } + let otherCompFinishLoading: () => void = () => {} + const OtherComponentLazy = React.lazy( + () => + new Promise<{ default: React.ComponentType