Skip to content

Commit 9737d3c

Browse files
author
Brian Vaughn
committed
Add edge case guard against subsction callback being called after unmount
1 parent 89ea65b commit 9737d3c

File tree

2 files changed

+74
-8
lines changed

2 files changed

+74
-8
lines changed

packages/create-subscription/src/__tests__/useSubscription-test.internal.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,57 @@ describe('useSubscription', () => {
354354
'Parent.componentDidUpdate',
355355
]);
356356
});
357+
358+
it('should guard against updates that happen after unmounting', () => {
359+
const Subscription = createSubscription({
360+
getCurrentValue: source => source.getValue(),
361+
subscribe: (source, callback) => {
362+
return source.subscribe(callback);
363+
},
364+
});
365+
366+
const eventHandler = {
367+
_callbacks: [],
368+
_value: true,
369+
change(value) {
370+
eventHandler._value = value;
371+
const _callbacks = eventHandler._callbacks.slice(0);
372+
_callbacks.forEach(callback => callback(value));
373+
},
374+
getValue() {
375+
return eventHandler._value;
376+
},
377+
subscribe(callback) {
378+
eventHandler._callbacks.push(callback);
379+
return () => {
380+
eventHandler._callbacks.splice(
381+
eventHandler._callbacks.indexOf(callback),
382+
1,
383+
);
384+
};
385+
},
386+
};
387+
388+
eventHandler.subscribe(value => {
389+
if (value === false) {
390+
renderer.unmount();
391+
expect(Scheduler).toFlushAndYield([]);
392+
}
393+
});
394+
395+
const renderer = ReactTestRenderer.create(
396+
<Subscription source={eventHandler}>
397+
{({value}) => {
398+
Scheduler.yieldValue(value);
399+
return null;
400+
}}
401+
</Subscription>,
402+
{unstable_isConcurrent: true},
403+
);
404+
405+
expect(Scheduler).toFlushAndYield([true]);
406+
407+
// This event should unmount
408+
eventHandler.change(false);
409+
});
357410
});

packages/create-subscription/src/useSubscription.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useEffect, useReducer, useState} from 'react';
1+
import {useEffect, useState} from 'react';
22

33
export function useSubscription({
44
// This is the thing being subscribed to (e.g. an observable, event dispatcher, etc).
@@ -39,25 +39,35 @@ export function useSubscription({
3939
// (Learn more at https://codesandbox.io/s/k0yvr5970o)
4040
useEffect(
4141
() => {
42+
let didUnsubscribe = false;
43+
4244
const checkForUpdates = () => {
43-
setState(state => {
45+
// It's possible that this callback will be invoked even after being unsubscribed,
46+
// if it's removed as a result of an event/update from the source.
47+
// In this case, React will log a DEV warning about an update from an unmounted component.
48+
// We can avoid triggering that warning with this check.
49+
if (didUnsubscribe) {
50+
return;
51+
}
52+
53+
setState(prevState => {
4454
// Ignore values from stale sources!
4555
// Since we subscribe an unsubscribe in a passive effect,
4656
// it's possible that this callback will be invoked for a stale (previous) source.
4757
// This check avoids scheduling an update for htat stale source.
48-
if (state.source !== source) {
49-
return state;
58+
if (prevState.source !== source) {
59+
return prevState;
5060
}
5161

5262
// Some subscription sources will auto-invoke the handler, even if the value hasn't changed.
5363
// If the value hasn't changed, no update is needed.
5464
// Return state as-is so React can bail out and avoid an unnecessary render.
5565
const value = getCurrentValue(source);
56-
if (state.value === value) {
57-
return state;
66+
if (prevState.value === value) {
67+
return prevState;
5868
}
5969

60-
return { ...state, value };
70+
return {...prevState, value};
6171
});
6272
};
6373
const unsubscribe = subscribe(source, checkForUpdates);
@@ -67,7 +77,10 @@ export function useSubscription({
6777
// Check for this and schedule an update if work has occurred.
6878
checkForUpdates();
6979

70-
return () => unsubscribe();
80+
return () => {
81+
didUnsubscribe = true;
82+
unsubscribe();
83+
};
7184
},
7285
[getCurrentValue, source, subscribe],
7386
);

0 commit comments

Comments
 (0)