Cleaup after use-markets hook

This commit is contained in:
Bartłomiej Głownia 2022-03-23 17:40:07 +01:00
parent 4698e532c1
commit f2e297ce39
13 changed files with 93 additions and 374 deletions

View File

@ -19,15 +19,7 @@
},
{
"files": ["*.js", "*.jsx"],
"rules": {
"unicorn/filename-case": [
"error",
{
"case": "kebabCase",
"ignore": ["react-singleton-hook/**/*.js"]
}
]
}
"rules": {}
}
],
"env": {

View File

@ -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;
};

View File

@ -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;
};

View File

@ -1,14 +0,0 @@
import { singletonHook } from './singletonHook';
import { SingletonHooksContainer } from './components/SingletonHooksContainer';
export {
singletonHook,
SingletonHooksContainer
};
const ReactSingletonHook = {
singletonHook,
SingletonHooksContainer
};
export default ReactSingletonHook;

View File

@ -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;
};
};

View File

@ -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.');
}
};

View File

@ -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.');
};

View File

@ -1,6 +0,0 @@
export const warning = (message) => {
if (console && console.warn) {
console.warn(message);
}
};

View File

@ -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 };
};

View File

@ -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,7 +24,9 @@ const Markets = () => {
const initialized = useRef<boolean>(false);
useEffect(() => {
return subscribe(client, ({ data, error, loading, delta }: CallbackArg) => {
return marketsDataProvider(
client,
({ data, error, loading, delta }: MarketsDataProviderCallbackArg) => {
setError(error);
setLoading(loading);
if (!error && !loading) {
@ -62,7 +64,8 @@ const Markets = () => {
}
}
}
});
}
);
}, [client, initialized]);
return (

View File

@ -0,0 +1 @@
export * from './markets-data-provider';

View File

@ -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) {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
data = 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;
}

View File

@ -14,3 +14,5 @@ export * from './__generated__/Orders';
export * from './__generated__/OrderSub';
export * from './__generated__/PartyAssetsQuery';
export * from './__generated__/ProposalsQuery';
export * from './data-providers';