From 319d3adf23f021c51c17aca58f4154337be9bcad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Fri, 18 Mar 2022 13:24:57 +0100 Subject: [PATCH 01/11] Unmount singleton hook if no consumers left --- apps/trading/.eslintrc.json | 10 +++- .../components/SingleItemContainer.js | 19 ++++++ .../components/SingletonHooksContainer.js | 60 +++++++++++++++++++ .../hooks/react-singleton-hook/index.js | 14 +++++ .../react-singleton-hook/singletonHook.js | 51 ++++++++++++++++ .../hooks/react-singleton-hook/utils/env.js | 22 +++++++ .../react-singleton-hook/utils/env.native.js | 9 +++ .../react-singleton-hook/utils/warning.js | 6 ++ apps/trading/hooks/use-markets.ts | 1 - apps/trading/pages/markets/index.page.tsx | 4 +- .../src/lib/market-list-table.spec.tsx | 2 +- 11 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js create mode 100644 apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js create mode 100644 apps/trading/hooks/react-singleton-hook/index.js create mode 100644 apps/trading/hooks/react-singleton-hook/singletonHook.js create mode 100644 apps/trading/hooks/react-singleton-hook/utils/env.js create mode 100644 apps/trading/hooks/react-singleton-hook/utils/env.native.js create mode 100644 apps/trading/hooks/react-singleton-hook/utils/warning.js diff --git a/apps/trading/.eslintrc.json b/apps/trading/.eslintrc.json index 2548ff963..49206a05e 100644 --- a/apps/trading/.eslintrc.json +++ b/apps/trading/.eslintrc.json @@ -19,7 +19,15 @@ }, { "files": ["*.js", "*.jsx"], - "rules": {} + "rules": { + "unicorn/filename-case": [ + "error", + { + "case": "kebabCase", + "ignore": ["react-singleton-hook/**/*.js"] + } + ] + } } ], "env": { diff --git a/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js b/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js new file mode 100644 index 000000000..54b4909b3 --- /dev/null +++ b/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js @@ -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; +}; diff --git a/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js b/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js new file mode 100644 index 000000000..c6bd2593b --- /dev/null +++ b/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js @@ -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) => )}; +}; + + +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; +}; diff --git a/apps/trading/hooks/react-singleton-hook/index.js b/apps/trading/hooks/react-singleton-hook/index.js new file mode 100644 index 000000000..b5494415e --- /dev/null +++ b/apps/trading/hooks/react-singleton-hook/index.js @@ -0,0 +1,14 @@ +import { singletonHook } from './singletonHook'; +import { SingletonHooksContainer } from './components/SingletonHooksContainer'; + +export { + singletonHook, + SingletonHooksContainer +}; + +const ReactSingletonHook = { + singletonHook, + SingletonHooksContainer +}; + +export default ReactSingletonHook; diff --git a/apps/trading/hooks/react-singleton-hook/singletonHook.js b/apps/trading/hooks/react-singleton-hook/singletonHook.js new file mode 100644 index 000000000..30d9e0130 --- /dev/null +++ b/apps/trading/hooks/react-singleton-hook/singletonHook.js @@ -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; + }; +}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/env.js b/apps/trading/hooks/react-singleton-hook/utils/env.js new file mode 100644 index 000000000..6c539c932 --- /dev/null +++ b/apps/trading/hooks/react-singleton-hook/utils/env.js @@ -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(, 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.'); + } +}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/env.native.js b/apps/trading/hooks/react-singleton-hook/utils/env.native.js new file mode 100644 index 000000000..8ce8c83ee --- /dev/null +++ b/apps/trading/hooks/react-singleton-hook/utils/env.native.js @@ -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.'); +}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/warning.js b/apps/trading/hooks/react-singleton-hook/utils/warning.js new file mode 100644 index 000000000..c644a7070 --- /dev/null +++ b/apps/trading/hooks/react-singleton-hook/utils/warning.js @@ -0,0 +1,6 @@ + +export const warning = (message) => { + if (console && console.warn) { + console.warn(message); + } +}; diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts index 88b60e49f..4c47bcaef 100644 --- a/apps/trading/hooks/use-markets.ts +++ b/apps/trading/hooks/use-markets.ts @@ -76,7 +76,6 @@ export const useMarkets = (): UseMarkets => { data: update, }; } - return m; }); }); diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index fd909eeca..6491648d3 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -22,4 +22,6 @@ const Markets = () => { ); }; -export default Markets; +const TwoMarkets = () => (<>
) + +export default TwoMarkets; diff --git a/libs/market-list/src/lib/market-list-table.spec.tsx b/libs/market-list/src/lib/market-list-table.spec.tsx index 1989c52d2..7dde7a630 100644 --- a/libs/market-list/src/lib/market-list-table.spec.tsx +++ b/libs/market-list/src/lib/market-list-table.spec.tsx @@ -6,7 +6,7 @@ describe('MarketListTable', () => { it('should render successfully', () => { const { baseElement } = render( - + ); expect(baseElement).toBeTruthy(); From f6e95392df6e9bf66c5fd46b380283605754501e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Tue, 22 Mar 2022 07:02:31 +0100 Subject: [PATCH 02/11] Add immer --- apps/trading/hooks/use-markets.ts | 18 ++++++++---------- package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts index 4c47bcaef..55e97fe1b 100644 --- a/apps/trading/hooks/use-markets.ts +++ b/apps/trading/hooks/use-markets.ts @@ -1,4 +1,5 @@ import { gql, useApolloClient } from '@apollo/client'; +import produce from 'immer'; import { Markets, Markets_markets, @@ -68,17 +69,14 @@ export const useMarkets = (): UseMarkets => { const [loading, setLoading] = useState(false); const mergeMarketData = useCallback((update: MarketDataSub_marketData) => { - setMarkets((curr) => { - return curr.map((m) => { - if (update.market.id === m.id) { - return { - ...m, - data: update, - }; + setMarkets((curr) => + produce(curr, (draft) => { + const index = draft.findIndex((m) => m.id === update.market.id); + if (index !== -1) { + draft[index].data = update; } - return m; - }); - }); + }) + ); }, []); // Make initial fetch diff --git a/package.json b/package.json index 8f0744025..33fe4b129 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "graphql": "^15.7.2", "graphql-ws": "^5.6.3", "lodash": "^4.17.21", + "immer": "^9.0.12", "next": "12.0.7", "nx": "^13.8.3", "postcss": "^8.4.6", diff --git a/yarn.lock b/yarn.lock index 7d5c3fae4..fecbd993b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12124,6 +12124,11 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= +immer@^9.0.12: + version "9.0.12" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" + integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== + immutable@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" From 15a788735158ceaa3183e103c8b1b905a62798f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Mon, 21 Mar 2022 14:14:09 +0100 Subject: [PATCH 03/11] Fix 0 handling in PriceCell --- .../src/lib/grid-cells/flash-cell.stories.tsx | 21 ++++++++++++ .../src/lib/grid-cells/flash-cell.test.tsx | 34 +++++++++++++++++++ .../src/lib/grid-cells/price-cell.test.tsx | 21 ++++++++++++ .../src/lib/grid-cells/price-cell.tsx | 9 +++-- 4 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 libs/react-helpers/src/lib/grid-cells/flash-cell.stories.tsx create mode 100644 libs/react-helpers/src/lib/grid-cells/flash-cell.test.tsx create mode 100644 libs/react-helpers/src/lib/grid-cells/price-cell.test.tsx diff --git a/libs/react-helpers/src/lib/grid-cells/flash-cell.stories.tsx b/libs/react-helpers/src/lib/grid-cells/flash-cell.stories.tsx new file mode 100644 index 000000000..6530aae60 --- /dev/null +++ b/libs/react-helpers/src/lib/grid-cells/flash-cell.stories.tsx @@ -0,0 +1,21 @@ +import { FlashCell } from './flash-cell'; +import { Meta, Story } from '@storybook/react'; +import * as React from 'react'; + +export default { + title: 'Component/FlashCell', + argTypes: { + value: { + control: { type: 'range', min: -20, max: 20, step: 1 }, + }, + }, +} as Meta; + +const Template: Story<{ value: number }> = ({ value }) => ( + {value.toFixed(0)} +); + +export const Basic = Template.bind({}); +Basic.args = { + value: 100, +}; diff --git a/libs/react-helpers/src/lib/grid-cells/flash-cell.test.tsx b/libs/react-helpers/src/lib/grid-cells/flash-cell.test.tsx new file mode 100644 index 000000000..14c598e82 --- /dev/null +++ b/libs/react-helpers/src/lib/grid-cells/flash-cell.test.tsx @@ -0,0 +1,34 @@ +import { findFirstDiffPos } from './flash-cell'; + +describe('findFirstDiffPos', () => { + it('Returns -1 for matching strings', () => { + const a = 'test'; + const b = 'test'; + + expect(findFirstDiffPos(a, b)).toEqual(-1); + }); + + it('Returns -1 if a string is undefined (just in case)', () => { + const a = 'test'; + const b = undefined as any as string; + + expect(findFirstDiffPos(a, b)).toEqual(-1); + expect(findFirstDiffPos(b, a)).toEqual(-1); + }); + + it('Returns -1 if one string is empty', () => { + const a = 'test'; + const b = ''; + + expect(findFirstDiffPos(a, b)).toEqual(-1); + expect(findFirstDiffPos(b, a)).toEqual(-1); + }); + + it('Happy path', () => { + const a = 'test'; + + expect(findFirstDiffPos(a, 'test')).toEqual(-1); + expect(findFirstDiffPos(a, '!est')).toEqual(0); + expect(findFirstDiffPos(a, 't!st')).toEqual(1); + }); +}); diff --git a/libs/react-helpers/src/lib/grid-cells/price-cell.test.tsx b/libs/react-helpers/src/lib/grid-cells/price-cell.test.tsx new file mode 100644 index 000000000..aec62c3b9 --- /dev/null +++ b/libs/react-helpers/src/lib/grid-cells/price-cell.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import * as React from 'react'; + +import { PriceCell } from './price-cell'; + +describe('', () => { + it('Displayes formatted value', () => { + render(); + expect(screen.getByTestId('price')).toHaveTextContent('100.00'); + }); + it('Displayes 0', () => { + render(); + expect(screen.getByTestId('price')).toHaveTextContent('0.00'); + }); + + it('Displayes - if value is not a number', () => { + render(); + expect(screen.getByTestId('price')).toHaveTextContent('-'); + }); +}); diff --git a/libs/react-helpers/src/lib/grid-cells/price-cell.tsx b/libs/react-helpers/src/lib/grid-cells/price-cell.tsx index 27daa50d1..8a94fc1f1 100644 --- a/libs/react-helpers/src/lib/grid-cells/price-cell.tsx +++ b/libs/react-helpers/src/lib/grid-cells/price-cell.tsx @@ -6,12 +6,11 @@ export interface IPriceCellProps { } export const PriceCell = ({ value, valueFormatted }: IPriceCellProps) => { - if (!value || isNaN(Number(value))) return -; + if ((!value && value !== 0) || isNaN(Number(value))) + return -; return ( - - - {valueFormatted} - + + {valueFormatted} ); }; From 81763c1e7061f4a8a4bf8483125ef1895b509cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Mon, 21 Mar 2022 17:17:29 +0100 Subject: [PATCH 04/11] Move apollo/react-testing to dev dependencies: --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33fe4b129..e1f981a5b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "private": true, "dependencies": { "@apollo/client": "^3.5.8", - "@apollo/react-testing": "^4.0.0", "@blueprintjs/icons": "^3.32.0", "@nrwl/next": "13.8.1", "@radix-ui/react-dialog": "^0.1.5", @@ -56,6 +55,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@apollo/react-testing": "^4.0.0", "@babel/core": "7.12.13", "@babel/preset-typescript": "7.12.13", "@nrwl/cli": "13.8.1", From 8b57f6fdb1005f93a575ef12a47bc212d18e2238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Mon, 21 Mar 2022 19:22:53 +0100 Subject: [PATCH 05/11] Fix RTL warnings --- .../src/lib/market-list-table.spec.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/libs/market-list/src/lib/market-list-table.spec.tsx b/libs/market-list/src/lib/market-list-table.spec.tsx index 7dde7a630..19d64ed6c 100644 --- a/libs/market-list/src/lib/market-list-table.spec.tsx +++ b/libs/market-list/src/lib/market-list-table.spec.tsx @@ -1,14 +1,17 @@ import { render } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; import { MockedProvider } from '@apollo/react-testing'; import MarketListTable from './market-list-table'; describe('MarketListTable', () => { - it('should render successfully', () => { - const { baseElement } = render( - - - - ); - expect(baseElement).toBeTruthy(); + it('should render successfully', async () => { + await act(async () => { + const { baseElement } = render( + + + + ); + expect(baseElement).toBeTruthy(); + }); }); }); From 2c28c9dd2dd502778ff808fd436f289ccdf3f03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Wed, 23 Mar 2022 08:25:20 +0100 Subject: [PATCH 06/11] Test ag-grid optimization approach --- apps/trading/hooks/use-markets.ts | 18 +-- apps/trading/pages/index.page.tsx | 62 +++++++- apps/trading/pages/markets/index.page.tsx | 14 +- .../market-list/src/lib/market-list-table.tsx | 147 +++++++++--------- .../src/hooks/use-apply-grid-transaction.ts | 48 +++++- .../ag-grid/ag-grid-dynamic-themed.tsx | 21 ++- .../components/ag-grid/ag-grid-dynamic.tsx | 33 ++-- .../ag-grid/ag-grid-lazy-themed.tsx | 21 ++- .../src/components/ag-grid/ag-grid-lazy.tsx | 10 +- 9 files changed, 237 insertions(+), 137 deletions(-) diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts index 55e97fe1b..e31cb67f5 100644 --- a/apps/trading/hooks/use-markets.ts +++ b/apps/trading/hooks/use-markets.ts @@ -62,7 +62,7 @@ interface UseMarkets { loading: boolean; } -export const useMarkets = (): UseMarkets => { +export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => void): UseMarkets => { const client = useApolloClient(); const [markets, setMarkets] = useState([]); const [error, setError] = useState(null); @@ -81,26 +81,21 @@ export const useMarkets = (): UseMarkets => { // Make initial fetch useEffect(() => { - const fetchOrders = async () => { + (async () => { setLoading(true); - try { const res = await client.query({ query: MARKETS_QUERY, }); - if (!res.data.markets?.length) return; - setMarkets(res.data.markets); } catch (err) { setError(err); } finally { setLoading(false); } - }; - - fetchOrders(); - }, [mergeMarketData, client]); + })(); + }, [client]); // Start subscription useEffect(() => { @@ -111,6 +106,9 @@ export const useMarkets = (): UseMarkets => { query: MARKET_DATA_SUB, }) .subscribe(({ data }) => { + if (updateCallback) { + updateCallback(data.marketData); + } mergeMarketData(data.marketData); }); @@ -119,7 +117,7 @@ export const useMarkets = (): UseMarkets => { sub.unsubscribe(); } }; - }, [client, mergeMarketData]); + }, [client, mergeMarketData, updateCallback]); return { markets, error, loading }; }; diff --git a/apps/trading/pages/index.page.tsx b/apps/trading/pages/index.page.tsx index 3377e03af..8b470e610 100644 --- a/apps/trading/pages/index.page.tsx +++ b/apps/trading/pages/index.page.tsx @@ -4,14 +4,68 @@ import { Callout, Intent, } from '@vegaprotocol/ui-toolkit'; +import type { GridApi } from 'ag-grid-community'; import { AgGridColumn } from 'ag-grid-react'; +import { useState, useEffect, useRef } from 'react'; +import { useApplyGridTransaction } from '@vegaprotocol/react-helpers'; -export function Index() { +const Grid = () => { const rowData = [ { make: 'Toyota', model: 'Celica', price: 35000 }, { make: 'Ford', model: 'Mondeo', price: 32000 }, { make: 'Porsche', model: 'Boxter', price: 72000 }, ]; + const ref = useRef(rowData); + const getRowNodeId = (data: { make: string }) => data.make; + const gridApi = useRef(null); + useEffect(() => { + const interval = setInterval(() => { + if (!gridApi) return; + const update = []; + const add = []; + + // split into updates and adds + [...rowData].forEach((data) => { + if (!gridApi.current) return; + + const rowNode = gridApi.current.getRowNode(getRowNodeId(data)); + + if (rowNode) { + if (rowNode.data !== data) { + update.push(data); + } + } else { + add.push(data); + } + }); + // async transaction for optimal handling of high grequency updates + if (update.length || add.length) { + gridApi.current.applyTransaction({ + update, + add, + addIndex: 0, + }); + } + }, 1000); + return () => clearInterval(interval); + }); + return ( + { + gridApi.current = params.api; + }} + getRowNodeId={getRowNodeId} + rowData={ref.current} + style={{ height: 400, width: 600 }} + > + + + + + ); +}; + +export function Index() { return (
@@ -29,11 +83,7 @@ export function Index() {
- - - - - + ); } diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index 6491648d3..dd7ad989f 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -3,10 +3,11 @@ import { useRouter } from 'next/router'; import { MarketListTable } from '@vegaprotocol/market-list'; import { useMarkets } from '../../hooks/use-markets'; import { AsyncRenderer } from '../../components/async-renderer'; +import { updateCallback } from '@vegaprotocol/react-helpers'; const Markets = () => { const { pathname, push } = useRouter(); - const { markets, error, loading } = useMarkets(); + const { markets, error, loading } = useMarkets(updateCallback); return ( @@ -22,6 +23,15 @@ const Markets = () => { ); }; -const TwoMarkets = () => (<>
) +const TwoMarkets = () => ( + <> +
+ +
+
+ +
+ +); export default TwoMarkets; diff --git a/libs/market-list/src/lib/market-list-table.tsx b/libs/market-list/src/lib/market-list-table.tsx index 89295fe20..4275dfeef 100644 --- a/libs/market-list/src/lib/market-list-table.tsx +++ b/libs/market-list/src/lib/market-list-table.tsx @@ -1,89 +1,82 @@ -import type { GridApi, ValueFormatterParams } from 'ag-grid-community'; -import { - PriceCell, - formatNumber, - useApplyGridTransaction, -} from '@vegaprotocol/react-helpers'; +import { forwardRef } from 'react'; +import type { ValueFormatterParams } from 'ag-grid-community'; +import { PriceCell, formatNumber } from '@vegaprotocol/react-helpers'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { Markets_markets } from '@vegaprotocol/graphql'; import { AgGridColumn } from 'ag-grid-react'; -import { useRef, useState } from 'react'; +import type { AgGridReact } from 'ag-grid-react'; +import { useState } from 'react'; interface MarketListTableProps { markets: Markets_markets[]; onRowClicked: (marketId: string) => void; } -export const MarketListTable = ({ - markets, - onRowClicked, -}: MarketListTableProps) => { - const [initialMarkets] = useState(markets); - const gridApi = useRef(null); - useApplyGridTransaction(markets, gridApi.current); +export const MarketListTable = forwardRef( + ({ markets, onRowClicked }, ref) => { + const [initialMarkets] = useState(markets); + const getRowNodeId = (data: Markets_markets) => data.id; - return ( - data.id} - suppressCellFocus={true} - defaultColDef={{ - flex: 1, - resizable: true, - }} - onGridReady={(params) => { - gridApi.current = params.api; - }} - onRowClicked={({ data }) => onRowClicked(data.id)} - components={{ PriceCell }} - > - - - - `${value.market.state} (${value.market.tradingMode})` - } - /> - - formatNumber(value, data.decimalPlaces) - } - /> - - formatNumber(value, data.decimalPlaces) - } - cellRenderer="PriceCell" - /> - - formatNumber(value, data.decimalPlaces) - } - /> - - - ); -}; + return ( + onRowClicked(data.id)} + components={{ PriceCell }} + > + + + + `${value.market.state} (${value.market.tradingMode})` + } + /> + + formatNumber(value, data.decimalPlaces) + } + /> + + formatNumber(value, data.decimalPlaces) + } + cellRenderer="PriceCell" + /> + + formatNumber(value, data.decimalPlaces) + } + /> + + + ); + } +); export default MarketListTable; diff --git a/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts b/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts index 007f891a7..082811bd9 100644 --- a/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts +++ b/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts @@ -1,10 +1,48 @@ import { GridApi } from 'ag-grid-community'; import { useEffect } from 'react'; import isEqual from 'lodash/isEqual'; +import { produce } from 'immer'; -export const useApplyGridTransaction = ( +export const updateCallback = + ( + gridApiRef: { current: GridApi | null }, + getRowNodeId: (row: T) => string + ) => + (data: T[]) => { + if (!gridApiRef.current) return; + + const update: T[] = []; + const add: T[] = []; + + // split into updates and adds + data.forEach((d) => { + if (!gridApiRef.current) return; + + const rowNode = gridApiRef.current.getRowNode(getRowNodeId(d)); + + if (rowNode) { + if ( + produce(rowNode.data, (draft: T) => Object.assign(draft, d)) !== + rowNode.data + ) { + update.push(d); + } + } else { + add.push(d); + } + }); + // async transaction for optimal handling of high grequency updates + gridApiRef.current.applyTransactionAsync({ + update, + add, + addIndex: 0, + }); + }; + +export const useApplyGridTransaction = ( data: T[], - gridApi: GridApi | null + gridApi: GridApi | null, + getRowNodeId: (row: T) => string ) => { useEffect(() => { if (!gridApi) return; @@ -16,7 +54,7 @@ export const useApplyGridTransaction = ( data.forEach((d) => { if (!gridApi) return; - const rowNode = gridApi.getRowNode(d.id); + const rowNode = gridApi.getRowNode(getRowNodeId(d)); if (rowNode) { if (!isEqual(rowNode.data, d)) { @@ -26,11 +64,11 @@ export const useApplyGridTransaction = ( add.push(d); } }); - + // async transaction for optimal handling of high grequency updates gridApi.applyTransaction({ update, add, addIndex: 0, }); - }, [data, gridApi]); + }, [data, gridApi, getRowNodeId]); }; diff --git a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic-themed.tsx b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic-themed.tsx index 924b5f111..5d2ab5eef 100644 --- a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic-themed.tsx +++ b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic-themed.tsx @@ -15,14 +15,13 @@ const AgGridDarkTheme = dynamic<{ children: React.ReactElement }>( { ssr: false } ); -export const AgGridThemed = ({ - style, - className, - ...props -}: (AgGridReactProps | AgReactUiProps) & { - style?: React.CSSProperties; - className?: string; -}) => { +export const AgGridThemed = React.forwardRef< + AgGridReact, + (AgGridReactProps | AgReactUiProps) & { + style?: React.CSSProperties; + className?: string; + } +>(({ style, className, ...props }, ref) => { const theme = React.useContext(ThemeContext); return (
{theme === 'dark' ? ( - + ) : ( - + )}
); -}; +}); diff --git a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx index 37eb6e6d9..a20c841ca 100644 --- a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx +++ b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx @@ -1,15 +1,28 @@ +import * as React from 'react'; import dynamic from 'next/dynamic'; -import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react'; +import type { + AgGridReactProps, + AgReactUiProps, + AgGridReact, +} from 'ag-grid-react'; + +type Props = (AgGridReactProps | AgReactUiProps) & { + style?: React.CSSProperties; + className?: string; + ref?: React.Ref; +}; // https://stackoverflow.com/questions/69433673/nextjs-reactdomserver-does-not-yet-support-suspense -export const AgGridDynamic = dynamic< - (AgGridReactProps | AgReactUiProps) & { - style?: React.CSSProperties; - className?: string; +const AgGridDynamicInternal = dynamic( + () => import('./ag-grid-dynamic-themed').then((mod) => mod.AgGridThemed), + { + ssr: false, + // https://nextjs.org/docs/messages/invalid-dynamic-suspense + // suspense: true } ->(() => import('./ag-grid-dynamic-themed').then((mod) => mod.AgGridThemed), { - ssr: false, - // https://nextjs.org/docs/messages/invalid-dynamic-suspense - // suspense: true -}); +); + +export const AgGridDynamic = React.forwardRef((props, ref) => ( + +)); diff --git a/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy-themed.tsx b/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy-themed.tsx index 54da4a860..8d8b160e9 100644 --- a/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy-themed.tsx +++ b/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy-themed.tsx @@ -16,14 +16,13 @@ const AgGridDarkTheme = React.lazy(() => })) ); -export const AgGridThemed = ({ - style, - className, - ...props -}: (AgGridReactProps | AgReactUiProps) & { - style?: React.CSSProperties; - className?: string; -}) => { +export const AgGridThemed = React.forwardRef< + AgGridReact, + (AgGridReactProps | AgReactUiProps) & { + style?: React.CSSProperties; + className?: string; + } +>(({ style, className, ...props }, ref) => { const theme = React.useContext(ThemeContext); return (
{theme === 'dark' ? ( - + ) : ( - + )}
); -}; +}); diff --git a/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy.tsx b/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy.tsx index 9d73d49ad..75bc5218b 100644 --- a/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy.tsx +++ b/libs/ui-toolkit/src/components/ag-grid/ag-grid-lazy.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; -import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react'; +import type { AgGridReact } from 'ag-grid-react'; -const LazyAgGridStyled = React.lazy(() => +export const AgGridLazyInternal = React.lazy(() => import('./ag-grid-lazy-themed').then((module) => ({ default: module.AgGridThemed, })) ); -export const AgGridLazy = ( - props: (AgGridReactProps | AgReactUiProps) & { style: React.CSSProperties } -) => ; +export const AgGridLazy = React.forwardRef((props, ref) => ( + +)); From 4698e532c1774c64432507915fe3afaf6b369e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Wed, 23 Mar 2022 13:47:45 +0100 Subject: [PATCH 07/11] Use data markets data provider instead of use-markets hook --- .../data-providers/markets-data-provider.ts | 166 ++++++++++++++++++ apps/trading/hooks/use-markets.ts | 16 +- apps/trading/pages/markets/index.page.tsx | 82 +++++++-- .../market-list/src/lib/market-list-table.tsx | 11 +- .../ag-grid/ag-grid-dynamic-themed.tsx | 23 +-- .../components/ag-grid/ag-grid-dynamic.tsx | 8 +- package.json | 1 + yarn.lock | 12 ++ 8 files changed, 277 insertions(+), 42 deletions(-) create mode 100644 apps/trading/data-providers/markets-data-provider.ts diff --git a/apps/trading/data-providers/markets-data-provider.ts b/apps/trading/data-providers/markets-data-provider.ts new file mode 100644 index 000000000..28c97ff3b --- /dev/null +++ b/apps/trading/data-providers/markets-data-provider.ts @@ -0,0 +1,166 @@ +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, + MarketDataSub, + MarketDataSub_marketData, +} from '@vegaprotocol/graphql'; + +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 + } + } +`; + +export interface CallbackArg { + data?: Markets_markets[]; + error?: Error; + loading: boolean; + delta?: MarketDataSub_marketData; +} + +export interface Callback { + (arg: CallbackArg): void; +} + +const callbacks: Callback[] = []; +const updateQueue: MarketDataSub_marketData[] = []; + +let data: Markets_markets[] = undefined; +let error: Error = undefined; +let loading = false; +let client: ApolloClient = undefined; +let subscription: Subscription = undefined; + +const notify = (callback, delta?: MarketDataSub_marketData) => { + callback({ + data, + error, + loading, + delta, + }); +}; + +const notifyAll = (delta?: MarketDataSub_marketData) => { + callbacks.forEach((callback) => notify(callback, delta)); +}; + +const update = (draft: Markets_markets[], delta: MarketDataSub_marketData) => { + const index = draft.findIndex((m) => m.id === delta.market.id); + if (index !== -1) { + draft[index].data = delta; + } + // @TODO - else push new market to draft +}; + +const initialize = async () => { + if (subscription) { + return; + } + loading = true; + error = null; + notifyAll(); + subscription = client + .subscribe({ + query: MARKET_DATA_SUB, + }) + .subscribe(({ data: delta }) => { + if (loading) { + updateQueue.push(delta.marketData); + } else { + data = produce(data, (draft) => { + update(draft, delta.marketData); + }); + notifyAll(delta.marketData); + } + }); + try { + const res = await client.query({ + query: MARKETS_QUERY, + }); + data = res.data.markets; + if (updateQueue) { + data = produce(data, (draft) => { + while (updateQueue.length) { + update(draft, updateQueue.shift()); + } + }); + } + } catch (e) { + error = e; + subscription.unsubscribe(); + subscription = undefined; + } finally { + loading = false; + notifyAll(); + } +}; + +const unsubscribe = (callback: Callback) => { + callbacks.splice(callbacks.indexOf(callback), 1); + if (callbacks.length === 0) { + subscription.unsubscribe(); + subscription = undefined; + data = undefined; + error = undefined; + loading = false; + } +}; + +export const subscribe = (c: ApolloClient, callback) => { + if (!client) { + client = c; + } + callbacks.push(callback); + if (callbacks.length === 1) { + initialize(); + } else { + notify(callback); + } + return () => unsubscribe(callback); +}; diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts index e31cb67f5..aa54751d0 100644 --- a/apps/trading/hooks/use-markets.ts +++ b/apps/trading/hooks/use-markets.ts @@ -81,21 +81,26 @@ export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => // Make initial fetch useEffect(() => { - (async () => { + const fetchOrders = async () => { setLoading(true); + try { const res = await client.query({ query: MARKETS_QUERY, }); + if (!res.data.markets?.length) return; + setMarkets(res.data.markets); } catch (err) { setError(err); } finally { setLoading(false); } - })(); - }, [client]); + }; + + fetchOrders(); + }, [mergeMarketData, client]); // Start subscription useEffect(() => { @@ -106,9 +111,6 @@ export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => query: MARKET_DATA_SUB, }) .subscribe(({ data }) => { - if (updateCallback) { - updateCallback(data.marketData); - } mergeMarketData(data.marketData); }); @@ -117,7 +119,7 @@ export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => sub.unsubscribe(); } }; - }, [client, mergeMarketData, updateCallback]); + }, [client, mergeMarketData]); return { markets, error, loading }; }; diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index dd7ad989f..704a78305 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -1,18 +1,75 @@ -import { Markets } from '@vegaprotocol/graphql'; +import { useState, useEffect, useRef } from 'react'; +import { produce } from 'immer'; +import assign from 'assign-deep'; +import { useApolloClient } from '@apollo/client'; import { useRouter } from 'next/router'; -import { MarketListTable } from '@vegaprotocol/market-list'; -import { useMarkets } from '../../hooks/use-markets'; import { AsyncRenderer } from '../../components/async-renderer'; -import { updateCallback } from '@vegaprotocol/react-helpers'; +import { MarketListTable, getRowNodeId } from '@vegaprotocol/market-list'; +import { + Markets_markets, + Markets_markets_data +} 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 = () => { const { pathname, push } = useRouter(); - const { markets, error, loading } = useMarkets(updateCallback); + const [markets, setMarkets] = useState(undefined); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(undefined); + const client = useApolloClient(); + const gridRef = useRef(); + const initialized = useRef(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[] = []; + + // 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) + ); + 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, + }); + } + } + } + }); + }, [client, initialized]); return ( {(data) => ( push(`${pathname}/${id}?portfolio=orders&trade=orderbook`) @@ -23,15 +80,8 @@ const Markets = () => { ); }; -const TwoMarkets = () => ( - <> -
- -
-
- -
- -); +export default Markets; -export default TwoMarkets; +// const TwoMarkets = () => (<>
) + +// export default TwoMarkets; diff --git a/libs/market-list/src/lib/market-list-table.tsx b/libs/market-list/src/lib/market-list-table.tsx index 4275dfeef..a3a567a46 100644 --- a/libs/market-list/src/lib/market-list-table.tsx +++ b/libs/market-list/src/lib/market-list-table.tsx @@ -12,23 +12,24 @@ interface MarketListTableProps { onRowClicked: (marketId: string) => void; } +export const getRowNodeId = (data: { id: string }) => data.id; + export const MarketListTable = forwardRef( ({ markets, onRowClicked }, ref) => { - const [initialMarkets] = useState(markets); - const getRowNodeId = (data: Markets_markets) => data.id; - return ( onRowClicked(data.id)} + onRowClicked={({ data }: { data: Markets_markets }) => + onRowClicked(data.id) + } components={{ PriceCell }} > ( { ssr: false } ); -export const AgGridThemed = React.forwardRef< - AgGridReact, - (AgGridReactProps | AgReactUiProps) & { - style?: React.CSSProperties; - className?: string; - } ->(({ style, className, ...props }, ref) => { +export const AgGridThemed = ({ + style, + className, + gridRef, + ...props +}: (AgGridReactProps | AgReactUiProps) & { + style?: React.CSSProperties; + className?: string; + gridRef?: React.ForwardedRef; +}) => { const theme = React.useContext(ThemeContext); return (
{theme === 'dark' ? ( - + ) : ( - + )}
); -}); +}; diff --git a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx index a20c841ca..1fcc41129 100644 --- a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx +++ b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx @@ -10,7 +10,7 @@ import type { type Props = (AgGridReactProps | AgReactUiProps) & { style?: React.CSSProperties; className?: string; - ref?: React.Ref; + gridRef?: React.Ref; }; // https://stackoverflow.com/questions/69433673/nextjs-reactdomserver-does-not-yet-support-suspense @@ -23,6 +23,6 @@ const AgGridDynamicInternal = dynamic( } ); -export const AgGridDynamic = React.forwardRef((props, ref) => ( - -)); +export const AgGridDynamic = React.forwardRef( + (props, ref) => +); diff --git a/package.json b/package.json index e1f981a5b..11de924dd 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "ag-grid-community": "^27.0.1", "ag-grid-react": "^27.0.1", "apollo": "^2.33.9", + "assign-deep": "^1.0.1", "autoprefixer": "^10.4.2", "bignumber.js": "^9.0.2", "classnames": "^2.3.1", diff --git a/yarn.lock b/yarn.lock index fecbd993b..0cd26c6d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6727,11 +6727,23 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assign-deep@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/assign-deep/-/assign-deep-1.0.1.tgz#b6d21d74e2f28bf6592e4c0c541bed6ab59c5f27" + integrity sha512-CSXAX79mibneEYfqLT5FEmkqR5WXF+xDRjgQQuVf6wSCXCYU8/vHttPidNar7wJ5BFmKAo8Wei0rCtzb+M/yeA== + dependencies: + assign-symbols "^2.0.2" + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +assign-symbols@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-2.0.2.tgz#0fb9191dd9d617042746ecfc354f3a3d768a0c98" + integrity sha512-9sBQUQZMKFKcO/C3Bo6Rx4CQany0R0UeVcefNGRRdW2vbmaMOhV1sbmlXcQLcD56juLXbSGTBm0GGuvmrAF8pA== + ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" From f2e297ce39975cd59e5896b27366ce20d4d27802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Wed, 23 Mar 2022 17:40:07 +0100 Subject: [PATCH 08/11] Cleaup after use-markets hook --- apps/trading/.eslintrc.json | 10 +- .../components/SingleItemContainer.js | 19 --- .../components/SingletonHooksContainer.js | 60 --------- .../hooks/react-singleton-hook/index.js | 14 -- .../react-singleton-hook/singletonHook.js | 51 ------- .../hooks/react-singleton-hook/utils/env.js | 22 --- .../react-singleton-hook/utils/env.native.js | 9 -- .../react-singleton-hook/utils/warning.js | 6 - apps/trading/hooks/use-markets.ts | 125 ------------------ apps/trading/pages/markets/index.page.tsx | 73 +++++----- libs/graphql/src/data-providers/index.ts | 1 + .../data-providers/markets-data-provider.ts | 75 +++++++---- libs/graphql/src/index.ts | 2 + 13 files changed, 93 insertions(+), 374 deletions(-) delete mode 100644 apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js delete mode 100644 apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js delete mode 100644 apps/trading/hooks/react-singleton-hook/index.js delete mode 100644 apps/trading/hooks/react-singleton-hook/singletonHook.js delete mode 100644 apps/trading/hooks/react-singleton-hook/utils/env.js delete mode 100644 apps/trading/hooks/react-singleton-hook/utils/env.native.js delete mode 100644 apps/trading/hooks/react-singleton-hook/utils/warning.js delete mode 100644 apps/trading/hooks/use-markets.ts create mode 100644 libs/graphql/src/data-providers/index.ts rename {apps/trading => libs/graphql/src}/data-providers/markets-data-provider.ts (63%) diff --git a/apps/trading/.eslintrc.json b/apps/trading/.eslintrc.json index 49206a05e..2548ff963 100644 --- a/apps/trading/.eslintrc.json +++ b/apps/trading/.eslintrc.json @@ -19,15 +19,7 @@ }, { "files": ["*.js", "*.jsx"], - "rules": { - "unicorn/filename-case": [ - "error", - { - "case": "kebabCase", - "ignore": ["react-singleton-hook/**/*.js"] - } - ] - } + "rules": {} } ], "env": { diff --git a/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js b/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js deleted file mode 100644 index 54b4909b3..000000000 --- a/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js +++ /dev/null @@ -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; -}; diff --git a/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js b/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js deleted file mode 100644 index c6bd2593b..000000000 --- a/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js +++ /dev/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) => )}; -}; - - -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; -}; diff --git a/apps/trading/hooks/react-singleton-hook/index.js b/apps/trading/hooks/react-singleton-hook/index.js deleted file mode 100644 index b5494415e..000000000 --- a/apps/trading/hooks/react-singleton-hook/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import { singletonHook } from './singletonHook'; -import { SingletonHooksContainer } from './components/SingletonHooksContainer'; - -export { - singletonHook, - SingletonHooksContainer -}; - -const ReactSingletonHook = { - singletonHook, - SingletonHooksContainer -}; - -export default ReactSingletonHook; diff --git a/apps/trading/hooks/react-singleton-hook/singletonHook.js b/apps/trading/hooks/react-singleton-hook/singletonHook.js deleted file mode 100644 index 30d9e0130..000000000 --- a/apps/trading/hooks/react-singleton-hook/singletonHook.js +++ /dev/null @@ -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; - }; -}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/env.js b/apps/trading/hooks/react-singleton-hook/utils/env.js deleted file mode 100644 index 6c539c932..000000000 --- a/apps/trading/hooks/react-singleton-hook/utils/env.js +++ /dev/null @@ -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(, 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.'); - } -}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/env.native.js b/apps/trading/hooks/react-singleton-hook/utils/env.native.js deleted file mode 100644 index 8ce8c83ee..000000000 --- a/apps/trading/hooks/react-singleton-hook/utils/env.native.js +++ /dev/null @@ -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.'); -}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/warning.js b/apps/trading/hooks/react-singleton-hook/utils/warning.js deleted file mode 100644 index c644a7070..000000000 --- a/apps/trading/hooks/react-singleton-hook/utils/warning.js +++ /dev/null @@ -1,6 +0,0 @@ - -export const warning = (message) => { - if (console && console.warn) { - console.warn(message); - } -}; diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts deleted file mode 100644 index aa54751d0..000000000 --- a/apps/trading/hooks/use-markets.ts +++ /dev/null @@ -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([]); - const [error, setError] = useState(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({ - 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({ - query: MARKET_DATA_SUB, - }) - .subscribe(({ data }) => { - mergeMarketData(data.marketData); - }); - - return () => { - if (sub) { - sub.unsubscribe(); - } - }; - }, [client, mergeMarketData]); - - return { markets, error, loading }; -}; diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index 704a78305..28bfc35b4 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -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(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 ( diff --git a/libs/graphql/src/data-providers/index.ts b/libs/graphql/src/data-providers/index.ts new file mode 100644 index 000000000..8da2b0152 --- /dev/null +++ b/libs/graphql/src/data-providers/index.ts @@ -0,0 +1 @@ +export * from './markets-data-provider'; diff --git a/apps/trading/data-providers/markets-data-provider.ts b/libs/graphql/src/data-providers/markets-data-provider.ts similarity index 63% rename from apps/trading/data-providers/markets-data-provider.ts rename to libs/graphql/src/data-providers/markets-data-provider.ts index 28c97ff3b..6309de1db 100644 --- a/apps/trading/data-providers/markets-data-provider.ts +++ b/libs/graphql/src/data-providers/markets-data-provider.ts @@ -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 = undefined; -let subscription: Subscription = undefined; +let client: ApolloClient | 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({ 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, callback) => { +export const marketsDataProvider = ( + c: ApolloClient, + callback: MarketsDataProviderCallback +) => { if (!client) { client = c; } diff --git a/libs/graphql/src/index.ts b/libs/graphql/src/index.ts index 59c3a0077..37e47e81f 100644 --- a/libs/graphql/src/index.ts +++ b/libs/graphql/src/index.ts @@ -14,3 +14,5 @@ export * from './__generated__/Orders'; export * from './__generated__/OrderSub'; export * from './__generated__/PartyAssetsQuery'; export * from './__generated__/ProposalsQuery'; + +export * from './data-providers'; From 1db791987512360584b5c1c8dee59285a3ae788b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Wed, 23 Mar 2022 18:31:44 +0100 Subject: [PATCH 09/11] Code style fixes --- apps/trading/pages/markets/index.page.tsx | 4 ---- libs/react-helpers/src/lib/grid-cells/price-cell.tsx | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index 28bfc35b4..fb910f5f5 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -84,7 +84,3 @@ const Markets = () => { }; export default Markets; - -// const TwoMarkets = () => (<>
) - -// export default TwoMarkets; diff --git a/libs/react-helpers/src/lib/grid-cells/price-cell.tsx b/libs/react-helpers/src/lib/grid-cells/price-cell.tsx index 8a94fc1f1..1b9102784 100644 --- a/libs/react-helpers/src/lib/grid-cells/price-cell.tsx +++ b/libs/react-helpers/src/lib/grid-cells/price-cell.tsx @@ -6,8 +6,9 @@ export interface IPriceCellProps { } export const PriceCell = ({ value, valueFormatted }: IPriceCellProps) => { - if ((!value && value !== 0) || isNaN(Number(value))) + if ((!value && value !== 0) || isNaN(Number(value))) { return -; + } return ( {valueFormatted} From afd82a8e4573efe512ca9e3104bca5398f742310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Thu, 24 Mar 2022 14:52:33 +0100 Subject: [PATCH 10/11] Revert useApplyGridTransaction changes --- .../src/hooks/use-apply-grid-transaction.ts | 48 ++----------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts b/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts index 082811bd9..007f891a7 100644 --- a/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts +++ b/libs/react-helpers/src/hooks/use-apply-grid-transaction.ts @@ -1,48 +1,10 @@ import { GridApi } from 'ag-grid-community'; import { useEffect } from 'react'; import isEqual from 'lodash/isEqual'; -import { produce } from 'immer'; -export const updateCallback = - ( - gridApiRef: { current: GridApi | null }, - getRowNodeId: (row: T) => string - ) => - (data: T[]) => { - if (!gridApiRef.current) return; - - const update: T[] = []; - const add: T[] = []; - - // split into updates and adds - data.forEach((d) => { - if (!gridApiRef.current) return; - - const rowNode = gridApiRef.current.getRowNode(getRowNodeId(d)); - - if (rowNode) { - if ( - produce(rowNode.data, (draft: T) => Object.assign(draft, d)) !== - rowNode.data - ) { - update.push(d); - } - } else { - add.push(d); - } - }); - // async transaction for optimal handling of high grequency updates - gridApiRef.current.applyTransactionAsync({ - update, - add, - addIndex: 0, - }); - }; - -export const useApplyGridTransaction = ( +export const useApplyGridTransaction = ( data: T[], - gridApi: GridApi | null, - getRowNodeId: (row: T) => string + gridApi: GridApi | null ) => { useEffect(() => { if (!gridApi) return; @@ -54,7 +16,7 @@ export const useApplyGridTransaction = ( data.forEach((d) => { if (!gridApi) return; - const rowNode = gridApi.getRowNode(getRowNodeId(d)); + const rowNode = gridApi.getRowNode(d.id); if (rowNode) { if (!isEqual(rowNode.data, d)) { @@ -64,11 +26,11 @@ export const useApplyGridTransaction = ( add.push(d); } }); - // async transaction for optimal handling of high grequency updates + gridApi.applyTransaction({ update, add, addIndex: 0, }); - }, [data, gridApi, getRowNodeId]); + }, [data, gridApi]); }; From fae1b8a5eec572ed6104a459bd2091885d8016ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Thu, 24 Mar 2022 18:29:56 +0100 Subject: [PATCH 11/11] Cleanup after tests of ag-grid optimization --- apps/trading/pages/index.page.tsx | 62 +++-------------------- apps/trading/pages/markets/index.page.tsx | 6 +-- 2 files changed, 8 insertions(+), 60 deletions(-) diff --git a/apps/trading/pages/index.page.tsx b/apps/trading/pages/index.page.tsx index 8b470e610..3377e03af 100644 --- a/apps/trading/pages/index.page.tsx +++ b/apps/trading/pages/index.page.tsx @@ -4,68 +4,14 @@ import { Callout, Intent, } from '@vegaprotocol/ui-toolkit'; -import type { GridApi } from 'ag-grid-community'; import { AgGridColumn } from 'ag-grid-react'; -import { useState, useEffect, useRef } from 'react'; -import { useApplyGridTransaction } from '@vegaprotocol/react-helpers'; -const Grid = () => { +export function Index() { const rowData = [ { make: 'Toyota', model: 'Celica', price: 35000 }, { make: 'Ford', model: 'Mondeo', price: 32000 }, { make: 'Porsche', model: 'Boxter', price: 72000 }, ]; - const ref = useRef(rowData); - const getRowNodeId = (data: { make: string }) => data.make; - const gridApi = useRef(null); - useEffect(() => { - const interval = setInterval(() => { - if (!gridApi) return; - const update = []; - const add = []; - - // split into updates and adds - [...rowData].forEach((data) => { - if (!gridApi.current) return; - - const rowNode = gridApi.current.getRowNode(getRowNodeId(data)); - - if (rowNode) { - if (rowNode.data !== data) { - update.push(data); - } - } else { - add.push(data); - } - }); - // async transaction for optimal handling of high grequency updates - if (update.length || add.length) { - gridApi.current.applyTransaction({ - update, - add, - addIndex: 0, - }); - } - }, 1000); - return () => clearInterval(interval); - }); - return ( - { - gridApi.current = params.api; - }} - getRowNodeId={getRowNodeId} - rowData={ref.current} - style={{ height: 400, width: 600 }} - > - - - - - ); -}; - -export function Index() { return (
@@ -83,7 +29,11 @@ export function Index() {
- + + + + + ); } diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index fb910f5f5..f7a538413 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -51,10 +51,8 @@ const Markets = () => { if (updatedData !== rowNode.data.data) { update.push({ ...rowNode.data, data: delta }); } - } /* else { - add.push(d); - }*/ - // async transaction for optimal handling of high grequency updates + } + // @TODO - else add new market if (update.length || add.length) { gridRef.current.api.applyTransactionAsync({ update,