Skip to content

Commit 949aada

Browse files
authored
Add option to unmount hook if there are no consumers #458 (#459)
1 parent 00a1aff commit 949aada

File tree

4 files changed

+63
-7
lines changed

4 files changed

+63
-7
lines changed

src/components/SingletonHooksContainer.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ let SingletonHooksContainerRendered = false;
88
let SingletonHooksContainerMountedAutomatically = false;
99

1010
let mountQueue = [];
11-
const mountIntoContainerDefault = (item) => { mountQueue.push(item); };
11+
const mountIntoContainerDefault = (item) => {
12+
mountQueue.push(item);
13+
return () => {
14+
throw new Error('damn');
15+
// mountQueue = mountQueue.filter(i => i !== item);
16+
};
17+
};
1218
let mountIntoContainer = mountIntoContainerDefault;
1319

1420
export const SingletonHooksContainer = () => {
@@ -25,7 +31,12 @@ export const SingletonHooksContainer = () => {
2531
const [hooks, setHooks] = useState([]);
2632

2733
useEffect(() => {
28-
mountIntoContainer = item => setHooks(hooks => [...hooks, item]);
34+
mountIntoContainer = item => {
35+
setHooks(hooks => [...hooks, item]);
36+
return () => {
37+
setHooks(hooks => hooks.filter(i => i !== item));
38+
};
39+
};
2940
setHooks(mountQueue);
3041
}, []);
3142

@@ -38,7 +49,7 @@ export const addHook = hook => {
3849
SingletonHooksContainerMountedAutomatically = true;
3950
mount(SingletonHooksContainer);
4051
}
41-
mountIntoContainer(hook);
52+
return mountIntoContainer(hook);
4253
};
4354

4455
export const resetLocalStateForTests = () => {

src/singletonHook.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { useEffect, useState } from 'react';
22
import { addHook } from './components/SingletonHooksContainer';
33
import { batch } from './utils/env';
44

5-
export const singletonHook = (initValue, useHookBody) => {
5+
export const singletonHook = (initValue, useHookBody, unmountIfNoConsumers = false) => {
66
let mounted = false;
7+
let removeHook = undefined;
78
let initStateCalculated = false;
89
let lastKnownState = undefined;
910
let consumers = [];
@@ -27,14 +28,20 @@ export const singletonHook = (initValue, useHookBody) => {
2728
useEffect(() => {
2829
if (!mounted) {
2930
mounted = true;
30-
addHook({ initValue, useHookBody, applyStateChange });
31+
removeHook = addHook({ initValue, useHookBody, applyStateChange });
3132
}
3233

3334
consumers.push(setState);
3435
if (lastKnownState !== state) {
3536
setState(lastKnownState);
3637
}
37-
return () => { consumers.splice(consumers.indexOf(setState), 1); };
38+
return () => {
39+
consumers.splice(consumers.indexOf(setState), 1);
40+
if (consumers.length === 0 && unmountIfNoConsumers) {
41+
removeHook();
42+
mounted = false;
43+
}
44+
};
3845

3946
// eslint-disable-next-line react-hooks/exhaustive-deps
4047
}, []);

test/integration/integration.spec.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react';
22
import * as rtl from '@testing-library/react';
33
import { singletonHook } from '../../src';
4-
import { resetLocalStateForTests } from '../../src/components/SingletonHooksContainer';
4+
import { resetLocalStateForTests, SingletonHooksContainer } from '../../src/components/SingletonHooksContainer';
55

66
describe('singletonHook', () => {
77
afterEach(() => {
@@ -108,4 +108,23 @@ describe('singletonHook', () => {
108108
rtl.render(<Tmp/>);
109109
expect(messages).toEqual(['initVal', 'newVal']);
110110
});
111+
112+
it('unmounts hook if no consumers', () => {
113+
const unmountCallback = jest.fn();
114+
const initVal = 'initVal';
115+
const useHook = singletonHook(initVal, () => useEffect(() => unmountCallback), true);
116+
117+
const Tmp = () => {
118+
useHook();
119+
return null;
120+
};
121+
122+
rtl.render(<SingletonHooksContainer/>);
123+
const { unmount } = rtl.render(<Tmp/>);
124+
const { unmount: unmountLastInstance } = rtl.render(<Tmp/>);
125+
unmount();
126+
expect(unmountCallback.mock.calls.length).toBe(0);
127+
unmountLastInstance();
128+
expect(unmountCallback.mock.calls.length).toBe(1);
129+
});
111130
});

test/react-native/native.spec.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,23 @@ describe('singletonHook', () => {
9797
);
9898
expect(messages).toEqual(['initVal', 'newVal']);
9999
});
100+
101+
it('unmounts hook if no consumers', () => {
102+
const unmountCallback = jest.fn();
103+
const initVal = 'initVal';
104+
const useHook = singletonHook(initVal, () => useEffect(() => unmountCallback), true);
105+
106+
const Tmp = () => {
107+
useHook();
108+
return null;
109+
};
110+
111+
rtl.render(<SingletonHooksContainer/>);
112+
const { unmount } = rtl.render(<Tmp/>);
113+
const { unmount: unmountLastInstance } = rtl.render(<Tmp/>);
114+
unmount();
115+
expect(unmountCallback.mock.calls.length).toBe(0);
116+
unmountLastInstance();
117+
expect(unmountCallback.mock.calls.length).toBe(1);
118+
});
100119
});

0 commit comments

Comments
 (0)