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] 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) => ( + +));