Unmount singleton hook if no consumers left
This commit is contained in:
parent
56b6933ea3
commit
319d3adf23
@ -19,7 +19,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": ["*.js", "*.jsx"],
|
"files": ["*.js", "*.jsx"],
|
||||||
"rules": {}
|
"rules": {
|
||||||
|
"unicorn/filename-case": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"case": "kebabCase",
|
||||||
|
"ignore": ["react-singleton-hook/**/*.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"env": {
|
"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,
|
data: update,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return m;
|
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', () => {
|
it('should render successfully', () => {
|
||||||
const { baseElement } = render(
|
const { baseElement } = render(
|
||||||
<MockedProvider>
|
<MockedProvider>
|
||||||
<MarketListTable width={100} height={100} />
|
<MarketListTable />
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
expect(baseElement).toBeTruthy();
|
expect(baseElement).toBeTruthy();
|
||||||
|
Loading…
Reference in New Issue
Block a user