Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 18 migration and bugfixes #489

Merged
merged 1 commit into from
Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ with a module bundler like [Webpack](https://webpack.js.org/) or
[Browserify](http://browserify.org/) to consume [CommonJS
modules](https://webpack.js.org/api/module-methods/#commonjs).

### React 18 breaking change
`react-singleton-hook` version 4.0.0 starts using new React DOM API and is only compatible with react 18.
Please use 3.x.x if you have to stay on lover React versions.

## What is a singleton hook
- Singleton hooks very similar to React Context in terms of functionality. Each singleton hook has a body,
you might think of it as of Context Provider body. Hook has a return value, it's similar to the value provided by context.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-singleton-hook",
"version": "3.4.0",
"version": "4.0.0",
"description": "Share custom hook state across all components",
"keywords": [
"react",
Expand Down Expand Up @@ -36,7 +36,7 @@
"ncu:apply": "ncu --reject react -u"
},
"peerDependencies": {
"react": "15 - 18"
"react": "18"
},
"peerDependenciesMeta": {
"react-dom": {
Expand Down
103 changes: 64 additions & 39 deletions src/components/SingletonHooksContainer.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,86 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { SingleItemContainer } from './SingleItemContainer';
import { mount } from '../utils/env';
import { warning } from '../utils/warning';

let SingletonHooksContainerMounted = false;
let SingletonHooksContainerRendered = false;
let SingletonHooksContainerMountedAutomatically = false;
let nextKey = 1;
let automaticRender = false;
let manualRender = false;
const workingSet = [];
const renderedContainers = [];

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

export const SingletonHooksContainer = () => {
SingletonHooksContainerRendered = true;
export const SingletonHooksContainer = ({ automaticContainerInternalUseOnly }) => {
const [hooks, setHooks] = useState([]);
const currentHooksRef = useRef();
currentHooksRef.current = hooks;

// if there was no automaticRender, and this one is not automatic as well
if (!automaticContainerInternalUseOnly && automaticRender === false) {
manualRender = true;
}

useEffect(() => {
if (SingletonHooksContainerMounted) {
warning('SingletonHooksContainer is mounted second time. '
+ 'You should mount SingletonHooksContainer before any other component and never unmount it.'
+ 'Alternatively, dont use SingletonHooksContainer it at all, we will handle that for you.');
let mounted = true;

function updateRenderedHooks() {
if (!mounted) return;

if (renderedContainers[0] !== updateRenderedHooks) {
if (!automaticContainerInternalUseOnly && automaticRender === true) {
warning('SingletonHooksContainer is mounted after some singleton hook has been used.'
+ 'Your SingletonHooksContainer will not be used in favor of internal one.');
}
setHooks(_ => []);
return;
}

setHooks([...workingSet]);
}
SingletonHooksContainerMounted = true;
}, []);

const [hooks, setHooks] = useState([]);
renderedContainers.push(updateRenderedHooks);
notifyContainersAsync();

useEffect(() => {
mountIntoContainer = item => {
setHooks(hooks => [...hooks, item]);
return () => {
setHooks(hooks => hooks.filter(i => i !== item));
};
return () => {
mounted = false;

if (currentHooksRef.current.length > 0) {
warning('SingletonHooksContainer is unmounted, but it has active singleton hooks. '
+ 'They will be reevaluated once SingletonHooksContainer is mounted again');
}

renderedContainers.splice(renderedContainers.indexOf(updateRenderedHooks), 1);
notifyContainersAsync();
};
setHooks(mountQueue);
}, []);
}, [automaticContainerInternalUseOnly]);

return <>{hooks.map((h, i) => <SingleItemContainer {...h} key={i}/>)}</>;
return <>{hooks.map(({ hook, key }) => <SingleItemContainer {...hook} key={key}/>)}</>;
};


export const addHook = hook => {
if (!SingletonHooksContainerRendered && !SingletonHooksContainerMountedAutomatically) {
SingletonHooksContainerMountedAutomatically = true;
const key = nextKey++;
workingSet.push({ hook, key });

// no container and and no previous manually rendered containers
if (renderedContainers.length === 0 && manualRender === false) {
automaticRender = true;
mount(SingletonHooksContainer);
}
return mountIntoContainer(hook);

notifyContainersAsync();

return () => {
workingSet.splice(workingSet.findIndex(h => h.key === key), 1);
notifyContainersAsync();
};
};

export const resetLocalStateForTests = () => {
SingletonHooksContainerMounted = false;
SingletonHooksContainerRendered = false;
SingletonHooksContainerMountedAutomatically = false;
mountQueue = [];
mountIntoContainer = mountIntoContainerDefault;
automaticRender = false;
manualRender = false;
workingSet.splice(0, workingSet.length);
renderedContainers.splice(0, renderedContainers.length);
};
8 changes: 5 additions & 3 deletions src/utils/env.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
/* eslint-disable import/no-unresolved */
import { unstable_batchedUpdates, render } from 'react-dom';
import { createRoot } from 'react-dom/client';
import { unstable_batchedUpdates } from 'react-dom';
import { warning } from './warning';

// from https://github.com/purposeindustries/window-or-global/blob/master/lib/index.js
Expand All @@ -13,7 +13,9 @@ const globalObject = (typeof self === 'object' && self.self === self && self)
export const batch = cb => unstable_batchedUpdates(cb);
export const mount = C => {
if (globalObject.document && globalObject.document.createElement) {
render(<C/>, globalObject.document.createElement('div'));
const container = globalObject.document.createElement('div');
const root = createRoot(container);
root.render(<C automaticContainerInternalUseOnly={true}/>);
} else {
warning('Can not mount SingletonHooksContainer on server side. '
+ 'Did you manage to run useEffect on server? '
Expand Down
14 changes: 11 additions & 3 deletions test/components/SingletonHooksContainer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,24 @@ describe('SingletonHooksContainer', () => {
rtl.render(<SingletonHooksContainer/>);
});

it('second mount prints a warning', () => {
it('mount after automatic mount prints a warning a warning', () => {
let msg = '';
const spy = jest.spyOn(console, 'warn').mockImplementation(data => { msg += data; });

rtl.act(() => {
addHook({
initValue: 'hello',
applyStateChange: (_) => { },
useHookBody: () => { }
});
});

rtl.render(<div>
<SingletonHooksContainer/>
<SingletonHooksContainer/>
</div>);

spy.mockRestore();
expect(msg).toContain('SingletonHooksContainer is mounted second time');
expect(msg).toContain('Your SingletonHooksContainer will not be used in favor of internal one.');
});

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