Unmount singleton hook if no consumers left
This commit is contained in:
parent
56b6933ea3
commit
319d3adf23
@ -19,7 +19,15 @@
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"unicorn/filename-case": [
|
||||
"error",
|
||||
{
|
||||
"case": "kebabCase",
|
||||
"ignore": ["react-singleton-hook/**/*.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
|
19
apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js
vendored
Normal file
19
apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
|
||||
export const SingleItemContainer = ({ initValue, useHookBody, applyStateChange }) => {
|
||||
const lastState = useRef(initValue);
|
||||
if (typeof useHookBody !== 'function') {
|
||||
throw new Error(`function expected as hook body parameter. got ${typeof useHookBody}`);
|
||||
}
|
||||
const val = useHookBody();
|
||||
|
||||
//useLayoutEffect is safe from SSR perspective because SingleItemContainer should never be rendered on server
|
||||
useLayoutEffect(() => {
|
||||
if (lastState.current !== val) {
|
||||
lastState.current = val;
|
||||
applyStateChange(val);
|
||||
}
|
||||
}, [applyStateChange, val]);
|
||||
|
||||
return null;
|
||||
};
|
60
apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js
vendored
Normal file
60
apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useState, useEffect } 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 mountQueue = [];
|
||||
const mountIntoContainerDefault = (item) => {
|
||||
mountQueue.push(item);
|
||||
return () => {
|
||||
mountQueue = mountQueue.filter(i => i !== item);
|
||||
}
|
||||
};
|
||||
let mountIntoContainer = mountIntoContainerDefault;
|
||||
|
||||
export const SingletonHooksContainer = () => {
|
||||
SingletonHooksContainerRendered = 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.');
|
||||
}
|
||||
SingletonHooksContainerMounted = true;
|
||||
}, []);
|
||||
|
||||
const [hooks, setHooks] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
mountIntoContainer = item => {
|
||||
setHooks(hooks => [...hooks, item]);
|
||||
return () => {
|
||||
setHooks(hooks => hooks.filter(i => i !== item));
|
||||
}
|
||||
}
|
||||
setHooks(mountQueue);
|
||||
}, []);
|
||||
|
||||
return <>{hooks.map((h, i) => <SingleItemContainer {...h} key={i}/>)}</>;
|
||||
};
|
||||
|
||||
|
||||
export const addHook = hook => {
|
||||
if (!SingletonHooksContainerRendered && !SingletonHooksContainerMountedAutomatically) {
|
||||
SingletonHooksContainerMountedAutomatically = true;
|
||||
mount(SingletonHooksContainer);
|
||||
}
|
||||
return mountIntoContainer(hook);
|
||||
};
|
||||
|
||||
export const resetLocalStateForTests = () => {
|
||||
SingletonHooksContainerMounted = false;
|
||||
SingletonHooksContainerRendered = false;
|
||||
SingletonHooksContainerMountedAutomatically = false;
|
||||
mountQueue = [];
|
||||
mountIntoContainer = mountIntoContainerDefault;
|
||||
};
|
14
apps/trading/hooks/react-singleton-hook/index.js
vendored
Normal file
14
apps/trading/hooks/react-singleton-hook/index.js
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
import { singletonHook } from './singletonHook';
|
||||
import { SingletonHooksContainer } from './components/SingletonHooksContainer';
|
||||
|
||||
export {
|
||||
singletonHook,
|
||||
SingletonHooksContainer
|
||||
};
|
||||
|
||||
const ReactSingletonHook = {
|
||||
singletonHook,
|
||||
SingletonHooksContainer
|
||||
};
|
||||
|
||||
export default ReactSingletonHook;
|
51
apps/trading/hooks/react-singleton-hook/singletonHook.js
vendored
Normal file
51
apps/trading/hooks/react-singleton-hook/singletonHook.js
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { addHook } from './components/SingletonHooksContainer';
|
||||
import { batch } from './utils/env';
|
||||
|
||||
export const singletonHook = (initValue, useHookBody, unmount = false) => {
|
||||
let mounted = false;
|
||||
let removeHook = undefined
|
||||
let initStateCalculated = false;
|
||||
let lastKnownState = undefined;
|
||||
let consumers = [];
|
||||
|
||||
const applyStateChange = (newState) => {
|
||||
lastKnownState = newState;
|
||||
batch(() => consumers.forEach(c => c(newState)));
|
||||
};
|
||||
|
||||
const stateInitializer = () => {
|
||||
if (!initStateCalculated) {
|
||||
lastKnownState = typeof initValue === 'function' ? initValue() : initValue;
|
||||
initStateCalculated = true;
|
||||
}
|
||||
return lastKnownState;
|
||||
};
|
||||
|
||||
return () => {
|
||||
const [state, setState] = useState(stateInitializer);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) {
|
||||
mounted = true;
|
||||
removeHook = addHook({ initValue, useHookBody, applyStateChange });
|
||||
}
|
||||
|
||||
consumers.push(setState);
|
||||
if (lastKnownState !== state) {
|
||||
setState(lastKnownState);
|
||||
}
|
||||
return () => {
|
||||
consumers.splice(consumers.indexOf(setState), 1);
|
||||
if (consumers.length === 0 && unmount) {
|
||||
removeHook();
|
||||
mounted = false;
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
};
|
22
apps/trading/hooks/react-singleton-hook/utils/env.js
vendored
Normal file
22
apps/trading/hooks/react-singleton-hook/utils/env.js
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { unstable_batchedUpdates, render } from 'react-dom';
|
||||
import { warning } from './warning';
|
||||
|
||||
// from https://github.com/purposeindustries/window-or-global/blob/master/lib/index.js
|
||||
// avoid direct usage of 'window' because `window is not defined` error might happen in babel-node
|
||||
const globalObject = (typeof self === 'object' && self.self === self && self)
|
||||
|| (typeof global === 'object' && global.global === global && global)
|
||||
|| this;
|
||||
|
||||
|
||||
export const batch = cb => unstable_batchedUpdates(cb);
|
||||
export const mount = C => {
|
||||
if (globalObject.document && globalObject.document.createElement) {
|
||||
render(<C/>, globalObject.document.createElement('div'));
|
||||
} else {
|
||||
warning('Can not mount SingletonHooksContainer on server side. '
|
||||
+ 'Did you manage to run useEffect on server? '
|
||||
+ 'Please mount SingletonHooksContainer into your components tree manually.');
|
||||
}
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { unstable_batchedUpdates } from 'react-native';
|
||||
import { warning } from './warning';
|
||||
|
||||
export const batch = cb => unstable_batchedUpdates(cb);
|
||||
export const mount = C => {
|
||||
warning('Can not mount SingletonHooksContainer with react native.'
|
||||
+ 'Please mount SingletonHooksContainer into your components tree manually.');
|
||||
};
|
6
apps/trading/hooks/react-singleton-hook/utils/warning.js
vendored
Normal file
6
apps/trading/hooks/react-singleton-hook/utils/warning.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export const warning = (message) => {
|
||||
if (console && console.warn) {
|
||||
console.warn(message);
|
||||
}
|
||||
};
|
@ -76,7 +76,6 @@ export const useMarkets = (): UseMarkets => {
|
||||
data: update,
|
||||
};
|
||||
}
|
||||
|
||||
return m;
|
||||
});
|
||||
});
|
||||
|
@ -22,4 +22,6 @@ const Markets = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Markets;
|
||||
const TwoMarkets = () => (<><div style={{height: '50%'}}><Markets /></div><div style={{height: '50%'}}><Markets /></div></>)
|
||||
|
||||
export default TwoMarkets;
|
||||
|
@ -6,7 +6,7 @@ describe('MarketListTable', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(
|
||||
<MockedProvider>
|
||||
<MarketListTable width={100} height={100} />
|
||||
<MarketListTable />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
|
Loading…
Reference in New Issue
Block a user