Skip to content

Commit 2d9734c

Browse files
authored
React 18 migration and bugfixes (#489)
1 parent ee117b3 commit 2d9734c

File tree

5 files changed

+86
-47
lines changed

5 files changed

+86
-47
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ with a module bundler like [Webpack](https://webpack.js.org/) or
2525
[Browserify](http://browserify.org/) to consume [CommonJS
2626
modules](https://webpack.js.org/api/module-methods/#commonjs).
2727

28+
### React 18 breaking change
29+
`react-singleton-hook` version 4.0.0 starts using new React DOM API and is only compatible with react 18.
30+
Please use 3.x.x if you have to stay on lover React versions.
31+
2832
## What is a singleton hook
2933
- Singleton hooks very similar to React Context in terms of functionality. Each singleton hook has a body,
3034
you might think of it as of Context Provider body. Hook has a return value, it's similar to the value provided by context.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-singleton-hook",
3-
"version": "3.4.0",
3+
"version": "4.0.0",
44
"description": "Share custom hook state across all components",
55
"keywords": [
66
"react",
@@ -36,7 +36,7 @@
3636
"ncu:apply": "ncu --reject react -u"
3737
},
3838
"peerDependencies": {
39-
"react": "15 - 18"
39+
"react": "18"
4040
},
4141
"peerDependenciesMeta": {
4242
"react-dom": {
Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,86 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import { SingleItemContainer } from './SingleItemContainer';
33
import { mount } from '../utils/env';
44
import { warning } from '../utils/warning';
55

6-
let SingletonHooksContainerMounted = false;
7-
let SingletonHooksContainerRendered = false;
8-
let SingletonHooksContainerMountedAutomatically = false;
6+
let nextKey = 1;
7+
let automaticRender = false;
8+
let manualRender = false;
9+
const workingSet = [];
10+
const renderedContainers = [];
911

10-
let mountQueue = [];
11-
const mountIntoContainerDefault = (item) => {
12-
mountQueue.push(item);
13-
return () => {
14-
throw new Error('Can not unmount container! It is like a bug in react-singleton-hook library, because of unmountIfNoConsumers: true');
15-
// mountQueue = mountQueue.filter(i => i !== item);
16-
};
12+
const notifyContainersAsync = () => {
13+
renderedContainers.forEach(updateRenderedHooks => updateRenderedHooks());
1714
};
18-
let mountIntoContainer = mountIntoContainerDefault;
1915

20-
export const SingletonHooksContainer = () => {
21-
SingletonHooksContainerRendered = true;
16+
export const SingletonHooksContainer = ({ automaticContainerInternalUseOnly }) => {
17+
const [hooks, setHooks] = useState([]);
18+
const currentHooksRef = useRef();
19+
currentHooksRef.current = hooks;
20+
21+
// if there was no automaticRender, and this one is not automatic as well
22+
if (!automaticContainerInternalUseOnly && automaticRender === false) {
23+
manualRender = true;
24+
}
25+
2226
useEffect(() => {
23-
if (SingletonHooksContainerMounted) {
24-
warning('SingletonHooksContainer is mounted second time. '
25-
+ 'You should mount SingletonHooksContainer before any other component and never unmount it.'
26-
+ 'Alternatively, dont use SingletonHooksContainer it at all, we will handle that for you.');
27+
let mounted = true;
28+
29+
function updateRenderedHooks() {
30+
if (!mounted) return;
31+
32+
if (renderedContainers[0] !== updateRenderedHooks) {
33+
if (!automaticContainerInternalUseOnly && automaticRender === true) {
34+
warning('SingletonHooksContainer is mounted after some singleton hook has been used.'
35+
+ 'Your SingletonHooksContainer will not be used in favor of internal one.');
36+
}
37+
setHooks(_ => []);
38+
return;
39+
}
40+
41+
setHooks([...workingSet]);
2742
}
28-
SingletonHooksContainerMounted = true;
29-
}, []);
3043

31-
const [hooks, setHooks] = useState([]);
44+
renderedContainers.push(updateRenderedHooks);
45+
notifyContainersAsync();
3246

33-
useEffect(() => {
34-
mountIntoContainer = item => {
35-
setHooks(hooks => [...hooks, item]);
36-
return () => {
37-
setHooks(hooks => hooks.filter(i => i !== item));
38-
};
47+
return () => {
48+
mounted = false;
49+
50+
if (currentHooksRef.current.length > 0) {
51+
warning('SingletonHooksContainer is unmounted, but it has active singleton hooks. '
52+
+ 'They will be reevaluated once SingletonHooksContainer is mounted again');
53+
}
54+
55+
renderedContainers.splice(renderedContainers.indexOf(updateRenderedHooks), 1);
56+
notifyContainersAsync();
3957
};
40-
setHooks(mountQueue);
41-
}, []);
58+
}, [automaticContainerInternalUseOnly]);
4259

43-
return <>{hooks.map((h, i) => <SingleItemContainer {...h} key={i}/>)}</>;
60+
return <>{hooks.map(({ hook, key }) => <SingleItemContainer {...hook} key={key}/>)}</>;
4461
};
4562

46-
4763
export const addHook = hook => {
48-
if (!SingletonHooksContainerRendered && !SingletonHooksContainerMountedAutomatically) {
49-
SingletonHooksContainerMountedAutomatically = true;
64+
const key = nextKey++;
65+
workingSet.push({ hook, key });
66+
67+
// no container and and no previous manually rendered containers
68+
if (renderedContainers.length === 0 && manualRender === false) {
69+
automaticRender = true;
5070
mount(SingletonHooksContainer);
5171
}
52-
return mountIntoContainer(hook);
72+
73+
notifyContainersAsync();
74+
75+
return () => {
76+
workingSet.splice(workingSet.findIndex(h => h.key === key), 1);
77+
notifyContainersAsync();
78+
};
5379
};
5480

5581
export const resetLocalStateForTests = () => {
56-
SingletonHooksContainerMounted = false;
57-
SingletonHooksContainerRendered = false;
58-
SingletonHooksContainerMountedAutomatically = false;
59-
mountQueue = [];
60-
mountIntoContainer = mountIntoContainerDefault;
82+
automaticRender = false;
83+
manualRender = false;
84+
workingSet.splice(0, workingSet.length);
85+
renderedContainers.splice(0, renderedContainers.length);
6186
};

src/utils/env.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
2-
/* eslint-disable import/no-unresolved */
3-
import { unstable_batchedUpdates, render } from 'react-dom';
2+
import { createRoot } from 'react-dom/client';
3+
import { unstable_batchedUpdates } from 'react-dom';
44
import { warning } from './warning';
55

66
// from https://github.com/purposeindustries/window-or-global/blob/master/lib/index.js
@@ -13,7 +13,9 @@ const globalObject = (typeof self === 'object' && self.self === self && self)
1313
export const batch = cb => unstable_batchedUpdates(cb);
1414
export const mount = C => {
1515
if (globalObject.document && globalObject.document.createElement) {
16-
render(<C/>, globalObject.document.createElement('div'));
16+
const container = globalObject.document.createElement('div');
17+
const root = createRoot(container);
18+
root.render(<C automaticContainerInternalUseOnly={true}/>);
1719
} else {
1820
warning('Can not mount SingletonHooksContainer on server side. '
1921
+ 'Did you manage to run useEffect on server? '

test/components/SingletonHooksContainer.spec.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,24 @@ describe('SingletonHooksContainer', () => {
1212
rtl.render(<SingletonHooksContainer/>);
1313
});
1414

15-
it('second mount prints a warning', () => {
15+
it('mount after automatic mount prints a warning a warning', () => {
1616
let msg = '';
1717
const spy = jest.spyOn(console, 'warn').mockImplementation(data => { msg += data; });
18+
19+
rtl.act(() => {
20+
addHook({
21+
initValue: 'hello',
22+
applyStateChange: (_) => { },
23+
useHookBody: () => { }
24+
});
25+
});
26+
1827
rtl.render(<div>
1928
<SingletonHooksContainer/>
20-
<SingletonHooksContainer/>
2129
</div>);
2230

2331
spy.mockRestore();
24-
expect(msg).toContain('SingletonHooksContainer is mounted second time');
32+
expect(msg).toContain('Your SingletonHooksContainer will not be used in favor of internal one.');
2533
});
2634

2735
it('adds hooks to mounted container', () => {

0 commit comments

Comments
 (0)