Skip to content

Commit ae9c774

Browse files
authored
Grid performance improvements (#974)
1 parent 69d915e commit ae9c774

File tree

8 files changed

+78
-47
lines changed

8 files changed

+78
-47
lines changed

.changeset/big-beds-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ensembleui/react-runtime": patch
3+
---
4+
5+
Grid performance improvements

packages/framework/src/evaluate/binding.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { parseExpressionAt, tokTypes } from "acorn";
22
import type { Atom } from "jotai";
33
import { atom } from "jotai";
4-
import { isNil, merge, omitBy } from "lodash-es";
4+
import { groupBy, has, isNil, merge, omitBy } from "lodash-es";
55
import type { Expression } from "../shared";
66
import { isExpression, sanitizeJs, debug, error } from "../shared";
77
import {
@@ -13,14 +13,14 @@ import {
1313
themeAtom,
1414
envAtom,
1515
defaultScreenContext,
16-
screenDataAtom,
1716
screenInputAtom,
1817
widgetFamilyAtom,
1918
screenGlobalScriptAtom,
2019
screenImportScriptAtom,
2120
userAtom,
2221
appAtom,
2322
secretAtom,
23+
screenDataFamilyAtom,
2424
} from "../state";
2525
import { deviceAtom } from "../hooks/useDeviceObserver";
2626
import { evaluate } from "./evaluate";
@@ -62,16 +62,23 @@ export const createBindingAtom = <T = unknown>(
6262

6363
const dependencyEntries = identifiers.map((identifier) => {
6464
debug(`found dependency for ${String(widgetId)}: ${identifier}`);
65-
// TODO: Account for data bindings also
66-
const dependencyAtom = widgetFamilyAtom(identifier);
67-
return { name: identifier, dependencyAtom };
65+
const widgetDepAtom = widgetFamilyAtom(identifier);
66+
const dataDepAtom = screenDataFamilyAtom(identifier);
67+
// TODO: find a better way to distinguish data and widget identifiers
68+
return { name: identifier, dependencies: [dataDepAtom, widgetDepAtom] };
6869
});
6970

7071
const bindingAtom = atom((get) => {
71-
const data = get(screenDataAtom);
7272
const appData = get(appAtom);
73-
const valueEntries = dependencyEntries.map(({ name, dependencyAtom }) => {
74-
const value = get(dependencyAtom);
73+
const valueEntries = dependencyEntries.map(({ name, dependencies }) => {
74+
let value;
75+
for (const depAtom of dependencies) {
76+
const depValue = get<unknown>(depAtom);
77+
if (depValue) {
78+
value = depValue;
79+
break;
80+
}
81+
}
7582
debug(
7683
`value for dependency ${name} at ${String(widgetId)}: ${JSON.stringify(
7784
value,
@@ -80,6 +87,14 @@ export const createBindingAtom = <T = unknown>(
8087
return [name, value];
8188
});
8289

90+
const values = groupBy(valueEntries, ([, value]) => {
91+
// is widget state
92+
if (has(value, "values") || has(value, "invokables")) {
93+
return "widgets";
94+
}
95+
return "data";
96+
});
97+
8398
const evaluationContext = createEvaluationContext({
8499
applicationContext: {
85100
application: {
@@ -92,8 +107,8 @@ export const createBindingAtom = <T = unknown>(
92107
},
93108
screenContext: {
94109
inputs: get(screenInputAtom),
95-
widgets: omitBy(Object.fromEntries(valueEntries), isNil),
96-
data,
110+
widgets: omitBy(Object.fromEntries(values.widgets ?? []), isNil),
111+
data: omitBy(Object.fromEntries(values.data ?? []), isNil),
97112
},
98113
ensemble: {
99114
storage: createStorageApi(

packages/framework/src/state/screen.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { atom } from "jotai";
22
import { focusAtom } from "jotai-optics";
33
import { assign } from "lodash-es";
4+
import { atomFamily } from "jotai/utils";
45
import { type Response, type WebSocketConnection } from "../data";
56
import type { EnsembleAppModel, EnsembleScreenModel } from "../shared";
67
import type { WidgetState } from "./widget";
@@ -48,6 +49,10 @@ export const screenDataAtom = atom(
4849
},
4950
);
5051

52+
export const screenDataFamilyAtom = atomFamily((id: string) =>
53+
focusAtom(screenDataFocusAtom, (optics) => optics.path(id)),
54+
);
55+
5156
export const screenModelAtom = focusAtom(screenAtom, (optic) =>
5257
optic.prop("model"),
5358
);

packages/runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"lodash-es": "^4.17.21",
5151
"react-chartjs-2": "^5.2.0",
5252
"react-easy-crop": "^5.0.5",
53+
"react-fast-compare": "^3.2.2",
5354
"react-markdown": "^8.0.7",
5455
"react-resizable": "^3.0.5",
5556
"react-router-dom": "^6.16.0",

packages/runtime/src/widgets/DataGrid/DataCell.tsx

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CustomScopeProvider,
55
useTemplateData,
66
} from "@ensembleui/react-framework";
7+
import { memo } from "react";
78
import { EnsembleRuntime } from "../../runtime";
89
import type { DataGridRowTemplate } from "./DataGrid";
910

@@ -14,42 +15,41 @@ export interface DataCellProps {
1415
columnIndex: number;
1516
rowIndex: number;
1617
}
17-
export const DataCell: React.FC<DataCellProps> = ({
18-
template,
19-
columnIndex,
20-
rowIndex,
21-
data,
22-
}) => {
23-
const { "item-template": itemTemplate, children } = template.properties;
24-
const { namedData } = useTemplateData({
25-
...itemTemplate,
26-
context: data,
27-
});
18+
export const DataCell: React.FC<DataCellProps> = memo(
19+
({ template, columnIndex, rowIndex, data }) => {
20+
const { "item-template": itemTemplate, children } = template.properties;
21+
const { namedData } = useTemplateData({
22+
...itemTemplate,
23+
context: data,
24+
});
2825

29-
if (children) {
30-
return (
31-
<CustomScopeProvider
32-
value={{ ...(data as object), index: rowIndex } as CustomScope}
33-
>
34-
{EnsembleRuntime.render([children[columnIndex]])}
35-
</CustomScopeProvider>
36-
);
37-
}
26+
if (children) {
27+
return (
28+
<CustomScopeProvider
29+
value={{ ...(data as object), index: rowIndex } as CustomScope}
30+
>
31+
{EnsembleRuntime.render([children[columnIndex]])}
32+
</CustomScopeProvider>
33+
);
34+
}
3835

39-
if (isObject(itemTemplate) && !isEmpty(namedData)) {
40-
return (
41-
<CustomScopeProvider
42-
value={
43-
{
44-
...namedData[columnIndex],
45-
index: rowIndex,
46-
} as CustomScope
47-
}
48-
>
49-
{EnsembleRuntime.render([itemTemplate.template])}
50-
</CustomScopeProvider>
51-
);
52-
}
36+
if (isObject(itemTemplate) && !isEmpty(namedData)) {
37+
return (
38+
<CustomScopeProvider
39+
value={
40+
{
41+
...namedData[columnIndex],
42+
index: rowIndex,
43+
} as CustomScope
44+
}
45+
>
46+
{EnsembleRuntime.render([itemTemplate.template])}
47+
</CustomScopeProvider>
48+
);
49+
}
5350

54-
return null;
55-
};
51+
return null;
52+
},
53+
);
54+
55+
DataCell.displayName = "DataCell";

packages/runtime/src/widgets/DataGrid/DataGrid.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Table, type TableProps } from "antd";
22
import type { SorterResult } from "antd/es/table/interface";
3+
import isEqual from "react-fast-compare";
34
import {
45
Resizable,
56
type ResizableProps,
@@ -547,6 +548,7 @@ export const DataGrid: React.FC<GridProps> = (props) => {
547548
/>
548549
);
549550
}}
551+
shouldCellUpdate={(record, prev) => !isEqual(record, prev)}
550552
sorter={
551553
col.sort?.compareFn
552554
? (a, b): number =>

packages/runtime/src/widgets/PopupMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ export const PopupMenu: React.FC<PopupMenuProps> = ({
199199
}}
200200
trigger={[values?.trigger || DEFAULT_POPUPMENU_TRIGGER]}
201201
>
202-
<div>{widgetToRender}</div>
202+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
203+
<div onClick={(e) => e.stopPropagation()}>{widgetToRender}</div>
203204
</AntdDropdown>
204205
</div>
205206
);

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)