Cleaup after use-markets hook
This commit is contained in:
parent
4698e532c1
commit
f2e297ce39
@ -19,15 +19,7 @@
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {
|
||||
"unicorn/filename-case": [
|
||||
"error",
|
||||
{
|
||||
"case": "kebabCase",
|
||||
"ignore": ["react-singleton-hook/**/*.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
"rules": {}
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
|
@ -1,19 +0,0 @@
|
||||
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;
|
||||
};
|
@ -1,60 +0,0 @@
|
||||
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
14
apps/trading/hooks/react-singleton-hook/index.js
vendored
@ -1,14 +0,0 @@
|
||||
import { singletonHook } from './singletonHook';
|
||||
import { SingletonHooksContainer } from './components/SingletonHooksContainer';
|
||||
|
||||
export {
|
||||
singletonHook,
|
||||
SingletonHooksContainer
|
||||
};
|
||||
|
||||
const ReactSingletonHook = {
|
||||
singletonHook,
|
||||
SingletonHooksContainer
|
||||
};
|
||||
|
||||
export default ReactSingletonHook;
|
@ -1,51 +0,0 @@
|
||||
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;
|
||||
};
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
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.');
|
||||
}
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
/* 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.');
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
|
||||
export const warning = (message) => {
|
||||
if (console && console.warn) {
|
||||
console.warn(message);
|
||||
}
|
||||
};
|
@ -1,125 +0,0 @@
|
||||
import { gql, useApolloClient } from '@apollo/client';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
Markets,
|
||||
Markets_markets,
|
||||
MarketDataSub,
|
||||
MarketDataSub_marketData,
|
||||
} from '@vegaprotocol/graphql';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const MARKET_DATA_FRAGMENT = gql`
|
||||
fragment MarketDataFields on MarketData {
|
||||
market {
|
||||
id
|
||||
state
|
||||
tradingMode
|
||||
}
|
||||
bestBidPrice
|
||||
bestOfferPrice
|
||||
markPrice
|
||||
}
|
||||
`;
|
||||
|
||||
const MARKETS_QUERY = gql`
|
||||
${MARKET_DATA_FRAGMENT}
|
||||
query Markets {
|
||||
markets {
|
||||
id
|
||||
name
|
||||
decimalPlaces
|
||||
data {
|
||||
...MarketDataFields
|
||||
}
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
code
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MARKET_DATA_SUB = gql`
|
||||
${MARKET_DATA_FRAGMENT}
|
||||
subscription MarketDataSub {
|
||||
marketData {
|
||||
...MarketDataFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface UseMarkets {
|
||||
markets: Markets_markets[];
|
||||
error: Error | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => void): UseMarkets => {
|
||||
const client = useApolloClient();
|
||||
const [markets, setMarkets] = useState<Markets_markets[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const mergeMarketData = useCallback((update: MarketDataSub_marketData) => {
|
||||
setMarkets((curr) =>
|
||||
produce(curr, (draft) => {
|
||||
const index = draft.findIndex((m) => m.id === update.market.id);
|
||||
if (index !== -1) {
|
||||
draft[index].data = update;
|
||||
}
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Make initial fetch
|
||||
useEffect(() => {
|
||||
const fetchOrders = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await client.query<Markets>({
|
||||
query: MARKETS_QUERY,
|
||||
});
|
||||
|
||||
if (!res.data.markets?.length) return;
|
||||
|
||||
setMarkets(res.data.markets);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchOrders();
|
||||
}, [mergeMarketData, client]);
|
||||
|
||||
// Start subscription
|
||||
useEffect(() => {
|
||||
const sub = client
|
||||
// This data callback will unfortunately be called separately with an update for every market,
|
||||
// perhaps we should batch this somehow...
|
||||
.subscribe<MarketDataSub>({
|
||||
query: MARKET_DATA_SUB,
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
mergeMarketData(data.marketData);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (sub) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [client, mergeMarketData]);
|
||||
|
||||
return { markets, error, loading };
|
||||
};
|
@ -7,11 +7,11 @@ import { AsyncRenderer } from '../../components/async-renderer';
|
||||
import { MarketListTable, getRowNodeId } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
Markets_markets,
|
||||
Markets_markets_data
|
||||
Markets_markets_data,
|
||||
MarketsDataProviderCallbackArg,
|
||||
marketsDataProvider,
|
||||
} from '@vegaprotocol/graphql';
|
||||
|
||||
import { subscribe } from '../../data-providers/markets-data-provider';
|
||||
import type { CallbackArg } from '../../data-providers/markets-data-provider';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
|
||||
const Markets = () => {
|
||||
@ -24,45 +24,48 @@ const Markets = () => {
|
||||
const initialized = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
return subscribe(client, ({ data, error, loading, delta }: CallbackArg) => {
|
||||
setError(error);
|
||||
setLoading(loading);
|
||||
if (!error && !loading) {
|
||||
if (!initialized.current || !gridRef.current) {
|
||||
initialized.current = true;
|
||||
setMarkets(data);
|
||||
} else {
|
||||
const update: Markets_markets[] = [];
|
||||
const add: Markets_markets[] = [];
|
||||
return marketsDataProvider(
|
||||
client,
|
||||
({ data, error, loading, delta }: MarketsDataProviderCallbackArg) => {
|
||||
setError(error);
|
||||
setLoading(loading);
|
||||
if (!error && !loading) {
|
||||
if (!initialized.current || !gridRef.current) {
|
||||
initialized.current = true;
|
||||
setMarkets(data);
|
||||
} else {
|
||||
const update: Markets_markets[] = [];
|
||||
const add: Markets_markets[] = [];
|
||||
|
||||
// split into updates and adds
|
||||
if (!gridRef.current) return;
|
||||
const rowNode = gridRef.current.api.getRowNode(
|
||||
getRowNodeId(delta.market)
|
||||
);
|
||||
|
||||
if (rowNode) {
|
||||
const updatedData = produce(
|
||||
rowNode.data.data,
|
||||
(draft: Markets_markets_data) => assign(draft, delta)
|
||||
// split into updates and adds
|
||||
if (!gridRef.current) return;
|
||||
const rowNode = gridRef.current.api.getRowNode(
|
||||
getRowNodeId(delta.market)
|
||||
);
|
||||
if (updatedData !== rowNode.data.data) {
|
||||
update.push({ ...rowNode.data, data: delta });
|
||||
}
|
||||
} /* else {
|
||||
|
||||
if (rowNode) {
|
||||
const updatedData = produce(
|
||||
rowNode.data.data,
|
||||
(draft: Markets_markets_data) => assign(draft, delta)
|
||||
);
|
||||
if (updatedData !== rowNode.data.data) {
|
||||
update.push({ ...rowNode.data, data: delta });
|
||||
}
|
||||
} /* else {
|
||||
add.push(d);
|
||||
}*/
|
||||
// async transaction for optimal handling of high grequency updates
|
||||
if (update.length || add.length) {
|
||||
gridRef.current.api.applyTransactionAsync({
|
||||
update,
|
||||
add,
|
||||
addIndex: 0,
|
||||
});
|
||||
// async transaction for optimal handling of high grequency updates
|
||||
if (update.length || add.length) {
|
||||
gridRef.current.api.applyTransactionAsync({
|
||||
update,
|
||||
add,
|
||||
addIndex: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}, [client, initialized]);
|
||||
|
||||
return (
|
||||
|
1
libs/graphql/src/data-providers/index.ts
Normal file
1
libs/graphql/src/data-providers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './markets-data-provider';
|
@ -2,12 +2,12 @@ import { gql } from '@apollo/client';
|
||||
import { produce } from 'immer';
|
||||
import type { ApolloClient } from '@apollo/client';
|
||||
import type { Subscription } from 'zen-observable-ts';
|
||||
import { Markets, Markets_markets } from '../__generated__/Markets';
|
||||
|
||||
import {
|
||||
Markets,
|
||||
Markets_markets,
|
||||
MarketDataSub,
|
||||
MarketDataSub_marketData,
|
||||
} from '@vegaprotocol/graphql';
|
||||
} from '../__generated__/MarketDataSub';
|
||||
|
||||
const MARKET_DATA_FRAGMENT = gql`
|
||||
fragment MarketDataFields on MarketData {
|
||||
@ -57,27 +57,30 @@ const MARKET_DATA_SUB = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export interface CallbackArg {
|
||||
data?: Markets_markets[];
|
||||
export interface MarketsDataProviderCallbackArg {
|
||||
data: Markets_markets[] | null;
|
||||
error?: Error;
|
||||
loading: boolean;
|
||||
delta?: MarketDataSub_marketData;
|
||||
}
|
||||
|
||||
export interface Callback {
|
||||
(arg: CallbackArg): void;
|
||||
export interface MarketsDataProviderCallback {
|
||||
(arg: MarketsDataProviderCallbackArg): void;
|
||||
}
|
||||
|
||||
const callbacks: Callback[] = [];
|
||||
const callbacks: MarketsDataProviderCallback[] = [];
|
||||
const updateQueue: MarketDataSub_marketData[] = [];
|
||||
|
||||
let data: Markets_markets[] = undefined;
|
||||
let error: Error = undefined;
|
||||
let data: Markets_markets[] | null = null;
|
||||
let error: Error | undefined = undefined;
|
||||
let loading = false;
|
||||
let client: ApolloClient<object> = undefined;
|
||||
let subscription: Subscription = undefined;
|
||||
let client: ApolloClient<object> | undefined = undefined;
|
||||
let subscription: Subscription | undefined = undefined;
|
||||
|
||||
const notify = (callback, delta?: MarketDataSub_marketData) => {
|
||||
const notify = (
|
||||
callback: MarketsDataProviderCallback,
|
||||
delta?: MarketDataSub_marketData
|
||||
) => {
|
||||
callback({
|
||||
data,
|
||||
error,
|
||||
@ -90,7 +93,13 @@ const notifyAll = (delta?: MarketDataSub_marketData) => {
|
||||
callbacks.forEach((callback) => notify(callback, delta));
|
||||
};
|
||||
|
||||
const update = (draft: Markets_markets[], delta: MarketDataSub_marketData) => {
|
||||
const update = (
|
||||
draft: Markets_markets[] | null,
|
||||
delta: MarketDataSub_marketData
|
||||
) => {
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
const index = draft.findIndex((m) => m.id === delta.market.id);
|
||||
if (index !== -1) {
|
||||
draft[index].data = delta;
|
||||
@ -103,19 +112,29 @@ const initialize = async () => {
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
error = null;
|
||||
error = undefined;
|
||||
notifyAll();
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
subscription = client
|
||||
.subscribe<MarketDataSub>({
|
||||
query: MARKET_DATA_SUB,
|
||||
})
|
||||
.subscribe(({ data: delta }) => {
|
||||
if (!delta) {
|
||||
return;
|
||||
}
|
||||
if (loading) {
|
||||
updateQueue.push(delta.marketData);
|
||||
} else {
|
||||
data = produce(data, (draft) => {
|
||||
const newData = produce(data, (draft) => {
|
||||
update(draft, delta.marketData);
|
||||
});
|
||||
if (newData === data) {
|
||||
return;
|
||||
}
|
||||
data = newData;
|
||||
notifyAll(delta.marketData);
|
||||
}
|
||||
});
|
||||
@ -124,15 +143,18 @@ const initialize = async () => {
|
||||
query: MARKETS_QUERY,
|
||||
});
|
||||
data = res.data.markets;
|
||||
if (updateQueue) {
|
||||
if (updateQueue && updateQueue.length > 0) {
|
||||
data = produce(data, (draft) => {
|
||||
while (updateQueue.length) {
|
||||
update(draft, updateQueue.shift());
|
||||
const delta = updateQueue.shift();
|
||||
if (delta) {
|
||||
update(draft, delta);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
error = e;
|
||||
error = e as Error;
|
||||
subscription.unsubscribe();
|
||||
subscription = undefined;
|
||||
} finally {
|
||||
@ -141,18 +163,23 @@ const initialize = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = (callback: Callback) => {
|
||||
const unsubscribe = (callback: MarketsDataProviderCallback) => {
|
||||
callbacks.splice(callbacks.indexOf(callback), 1);
|
||||
if (callbacks.length === 0) {
|
||||
subscription.unsubscribe();
|
||||
subscription = undefined;
|
||||
data = undefined;
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
subscription = undefined;
|
||||
}
|
||||
data = null;
|
||||
error = undefined;
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const subscribe = (c: ApolloClient<object>, callback) => {
|
||||
export const marketsDataProvider = (
|
||||
c: ApolloClient<object>,
|
||||
callback: MarketsDataProviderCallback
|
||||
) => {
|
||||
if (!client) {
|
||||
client = c;
|
||||
}
|
@ -14,3 +14,5 @@ export * from './__generated__/Orders';
|
||||
export * from './__generated__/OrderSub';
|
||||
export * from './__generated__/PartyAssetsQuery';
|
||||
export * from './__generated__/ProposalsQuery';
|
||||
|
||||
export * from './data-providers';
|
||||
|
Loading…
Reference in New Issue
Block a user