Test ag-grid optimization approach

This commit is contained in:
Bartłomiej Głownia 2022-03-23 08:25:20 +01:00
parent 8b57f6fdb1
commit 2c28c9dd2d
9 changed files with 237 additions and 137 deletions

View File

@ -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<Markets_markets[]>([]);
const [error, setError] = useState<Error | null>(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<Markets>({
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 };
};

View File

@ -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<GridApi | null>(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 (
<AgGrid
onGridReady={(params) => {
gridApi.current = params.api;
}}
getRowNodeId={getRowNodeId}
rowData={ref.current}
style={{ height: 400, width: 600 }}
>
<AgGridColumn field="make"></AgGridColumn>
<AgGridColumn field="model"></AgGridColumn>
<AgGridColumn field="price"></AgGridColumn>
</AgGrid>
);
};
export function Index() {
return (
<div className="m-24">
<div className="mb-24">
@ -29,11 +83,7 @@ export function Index() {
</div>
</Callout>
</div>
<AgGrid rowData={rowData} style={{ height: 400, width: 600 }}>
<AgGridColumn field="make"></AgGridColumn>
<AgGridColumn field="model"></AgGridColumn>
<AgGridColumn field="price"></AgGridColumn>
</AgGrid>
<Grid />
</div>
);
}

View File

@ -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 (
<AsyncRenderer loading={loading} error={error} data={markets}>
@ -22,6 +23,15 @@ const Markets = () => {
);
};
const TwoMarkets = () => (<><div style={{height: '50%'}}><Markets /></div><div style={{height: '50%'}}><Markets /></div></>)
const TwoMarkets = () => (
<>
<div style={{ height: '50%' }}>
<Markets />
</div>
<div style={{ height: '50%' }}>
<Markets />
</div>
</>
);
export default TwoMarkets;

View File

@ -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<GridApi | null>(null);
useApplyGridTransaction<Markets_markets>(markets, gridApi.current);
export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
({ markets, onRowClicked }, ref) => {
const [initialMarkets] = useState(markets);
const getRowNodeId = (data: Markets_markets) => data.id;
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate="No markets"
rowData={initialMarkets}
getRowNodeId={(data) => data.id}
suppressCellFocus={true}
defaultColDef={{
flex: 1,
resizable: true,
}}
onGridReady={(params) => {
gridApi.current = params.api;
}}
onRowClicked={({ data }) => onRowClicked(data.id)}
components={{ PriceCell }}
>
<AgGridColumn
headerName="Market"
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName="Settlement asset"
field="tradableInstrument.instrument.product.settlementAsset.symbol"
/>
<AgGridColumn
headerName="State"
field="data"
valueFormatter={({ value }: ValueFormatterParams) =>
`${value.market.state} (${value.market.tradingMode})`
}
/>
<AgGridColumn
headerName="Best bid"
field="data.bestBidPrice"
type="rightAligned"
cellRenderer="PriceCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
formatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn
headerName="Best offer"
field="data.bestOfferPrice"
type="rightAligned"
valueFormatter={({ value, data }: ValueFormatterParams) =>
formatNumber(value, data.decimalPlaces)
}
cellRenderer="PriceCell"
/>
<AgGridColumn
headerName="Mark price"
field="data.markPrice"
type="rightAligned"
cellRenderer="PriceCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
formatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn headerName="Description" field="name" />
</AgGrid>
);
};
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate="No markets"
rowData={initialMarkets}
getRowNodeId={getRowNodeId}
ref={ref}
defaultColDef={{
flex: 1,
resizable: true,
}}
onRowClicked={({ data }) => onRowClicked(data.id)}
components={{ PriceCell }}
>
<AgGridColumn
headerName="Market"
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName="Settlement asset"
field="tradableInstrument.instrument.product.settlementAsset.symbol"
/>
<AgGridColumn
headerName="State"
field="data"
valueFormatter={({ value }: ValueFormatterParams) =>
`${value.market.state} (${value.market.tradingMode})`
}
/>
<AgGridColumn
headerName="Best bid"
field="data.bestBidPrice"
type="rightAligned"
cellRenderer="PriceCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
formatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn
headerName="Best offer"
field="data.bestOfferPrice"
type="rightAligned"
valueFormatter={({ value, data }: ValueFormatterParams) =>
formatNumber(value, data.decimalPlaces)
}
cellRenderer="PriceCell"
/>
<AgGridColumn
headerName="Mark price"
field="data.markPrice"
type="rightAligned"
cellRenderer="PriceCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
formatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn headerName="Description" field="name" />
</AgGrid>
);
}
);
export default MarketListTable;

View File

@ -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 = <T extends { id: string }>(
export const updateCallback =
<T>(
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 = <T>(
data: T[],
gridApi: GridApi | null
gridApi: GridApi | null,
getRowNodeId: (row: T) => string
) => {
useEffect(() => {
if (!gridApi) return;
@ -16,7 +54,7 @@ export const useApplyGridTransaction = <T extends { id: string }>(
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 = <T extends { id: string }>(
add.push(d);
}
});
// async transaction for optimal handling of high grequency updates
gridApi.applyTransaction({
update,
add,
addIndex: 0,
});
}, [data, gridApi]);
}, [data, gridApi, getRowNodeId]);
};

View File

@ -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 (
<div
@ -33,13 +32,13 @@ export const AgGridThemed = ({
>
{theme === 'dark' ? (
<AgGridDarkTheme>
<AgGridReact {...props} />
<AgGridReact {...props} ref={ref} />
</AgGridDarkTheme>
) : (
<AgGridLightTheme>
<AgGridReact {...props} />
<AgGridReact {...props} ref={ref} />
</AgGridLightTheme>
)}
</div>
);
};
});

View File

@ -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<AgGridReact>;
};
// 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<Props>(
() => 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<AgGridReact>((props, ref) => (
<AgGridDynamicInternal {...props} ref={ref} />
));

View File

@ -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 (
<div
@ -34,13 +33,13 @@ export const AgGridThemed = ({
>
{theme === 'dark' ? (
<AgGridDarkTheme>
<AgGridReact {...props} />
<AgGridReact {...props} ref={ref} />
</AgGridDarkTheme>
) : (
<AgGridLightTheme>
<AgGridReact {...props} />
<AgGridReact {...props} ref={ref} />
</AgGridLightTheme>
)}
</div>
);
};
});

View File

@ -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 }
) => <LazyAgGridStyled {...props} />;
export const AgGridLazy = React.forwardRef<AgGridReact>((props, ref) => (
<AgGridLazyInternal {...props} ref={ref} />
));