Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit dde6838

Browse files
committedMay 23, 2025·
table optimisations
1 parent b637bf6 commit dde6838

File tree

9 files changed

+153
-110
lines changed

9 files changed

+153
-110
lines changed
 

‎client/packages/lowcoder/src/app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const LazyDebugNewComp = React.lazy(() => import("./debugNew"));
7676
const Wrapper = (props: { children: React.ReactNode, language: string }) => (
7777
<ConfigProvider
7878
theme={{ hashed: false }}
79+
wave={{ disabled: true }}
7980
locale={getAntdLocale(props.language)}
8081
>
8182
<App>

‎client/packages/lowcoder/src/components/table/EditableCell.tsx

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface CellProps {
4646
cellTooltip?: string;
4747
editMode?: string;
4848
onTableEvent?: (eventName: any) => void;
49+
cellIndex?: string;
4950
}
5051

5152
export type CellViewReturn = (props: CellProps) => ReactNode;
@@ -65,7 +66,7 @@ const BorderDiv = styled.div`
6566
left: 0;
6667
`;
6768

68-
const CellWrapper = ({
69+
const CellWrapper = React.memo(({
6970
children,
7071
tooltipTitle,
7172
}: {
@@ -82,7 +83,7 @@ const CellWrapper = ({
8283
return (
8384
<>{children}</>
8485
)
85-
};
86+
});
8687

8788
interface EditableCellProps<T> extends CellProps {
8889
normalView: ReactNode;
@@ -108,6 +109,7 @@ function EditableCellComp<T extends JSONValue>(props: EditableCellProps<T>) {
108109
tableSize,
109110
textOverflow,
110111
cellTooltip,
112+
cellIndex,
111113
...otherProps
112114
} = props;
113115

@@ -118,15 +120,23 @@ function EditableCellComp<T extends JSONValue>(props: EditableCellProps<T>) {
118120
const [tmpValue, setTmpValue] = useState<T | null>(value);
119121
const singleClickEdit = editMode === 'single';
120122

121-
// Use refs to track previous values for comparison
123+
// Use refs to track component mount state and previous values
124+
const mountedRef = useRef(true);
122125
const prevValueRef = useRef(value);
123126

127+
// Cleanup on unmount
124128
useEffect(() => {
125-
console.log("rendered EditableCellComp");
126-
}, []);
129+
return () => {
130+
mountedRef.current = false;
131+
setTmpValue(null);
132+
setIsEditing(false);
133+
};
134+
}, [setIsEditing]);
127135

128136
// Update tmpValue when value changes
129137
useEffect(() => {
138+
if (!mountedRef.current) return;
139+
130140
if (!_.isEqual(value, prevValueRef.current)) {
131141
setTmpValue(value);
132142
prevValueRef.current = value;
@@ -135,12 +145,15 @@ function EditableCellComp<T extends JSONValue>(props: EditableCellProps<T>) {
135145

136146
const onChange = useCallback(
137147
(value: T) => {
148+
if (!mountedRef.current) return;
138149
setTmpValue(value);
139150
},
140151
[]
141152
);
142153

143154
const onChangeEnd = useCallback(() => {
155+
if (!mountedRef.current) return;
156+
144157
setIsEditing(false);
145158
const newValue = _.isNil(tmpValue) || _.isEqual(tmpValue, baseValue) ? null : tmpValue;
146159
dispatch(
@@ -153,32 +166,28 @@ function EditableCellComp<T extends JSONValue>(props: EditableCellProps<T>) {
153166
if(!_.isEqual(tmpValue, value)) {
154167
onTableEvent?.('columnEdited');
155168
}
156-
}, [dispatch, tmpValue, baseValue, value, onTableEvent]);
169+
}, [dispatch, tmpValue, baseValue, value, onTableEvent, setIsEditing]);
157170

158171
const editView = useMemo(
159172
() => editViewFn?.({ value, onChange, onChangeEnd, otherProps }) ?? <></>,
160173
[editViewFn, value, onChange, onChangeEnd, otherProps]
161174
);
162175

163176
const enterEditFn = useCallback(() => {
164-
if (editable) setIsEditing(true);
177+
if (!mountedRef.current || !editable) return;
178+
setIsEditing(true);
165179
}, [editable, setIsEditing]);
166180

167-
// Cleanup function
168-
useEffect(() => {
169-
return () => {
170-
// Reset state on unmount
171-
setTmpValue(null);
172-
setIsEditing(false);
173-
};
174-
}, [setIsEditing]);
175-
181+
// Memoize context values to prevent unnecessary re-renders
182+
const tagsContextValue = useMemo(() => candidateTags ?? [], [candidateTags]);
183+
const statusContextValue = useMemo(() => candidateStatus ?? [], [candidateStatus]);
184+
176185
if (isEditing) {
177186
return (
178187
<>
179188
<BorderDiv className="editing-border" />
180-
<TagsContext.Provider value={candidateTags ?? []}>
181-
<StatusContext.Provider value={candidateStatus ?? []}>
189+
<TagsContext.Provider value={tagsContextValue}>
190+
<StatusContext.Provider value={statusContextValue}>
182191
<div className="editing-wrapper">
183192
{editView}
184193
</div>
@@ -189,30 +198,30 @@ function EditableCellComp<T extends JSONValue>(props: EditableCellProps<T>) {
189198
}
190199

191200
return (
192-
<ColumnTypeView
193-
textOverflow={props.textOverflow}
194-
>
195-
{status === "toSave" && !isEditing && <EditableChip key={`editable-chip`}/>}
201+
<ColumnTypeView
202+
textOverflow={props.textOverflow}
203+
>
204+
{status === "toSave" && !isEditing && <EditableChip key={`editable-chip-${cellIndex}`}/>}
205+
<CellWrapper tooltipTitle={props.cellTooltip}>
206+
<div
207+
key={`normal-view-${cellIndex}`}
208+
tabIndex={editable ? 0 : -1 }
209+
onFocus={enterEditFn}
210+
>
211+
{normalView}
212+
</div>
213+
</CellWrapper>
214+
{/* overlay on normal view to handle double click for editing */}
215+
{editable && (
196216
<CellWrapper tooltipTitle={props.cellTooltip}>
197-
<div
198-
key={`normal-view`}
199-
tabIndex={editable ? 0 : -1 }
200-
onFocus={enterEditFn}
201-
>
202-
{normalView}
203-
</div>
204-
</CellWrapper>
205-
{/* overlay on normal view to handle double click for editing */}
206-
{editable && (
207-
<CellWrapper tooltipTitle={props.cellTooltip}>
208217
<EditableOverlay
209-
key="editable-view"
218+
key={`editable-view-${cellIndex}`}
210219
onDoubleClick={!singleClickEdit ? enterEditFn : undefined}
211220
onClick={singleClickEdit ? enterEditFn : undefined}
212221
/>
213222
</CellWrapper>
214-
)}
215-
</ColumnTypeView>
223+
)}
224+
</ColumnTypeView>
216225
);
217226
}
218227

‎client/packages/lowcoder/src/components/table/columnTypeView.tsx

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import styled from "styled-components";
33

4+
const overflowStyles = `
5+
div {
6+
overflow: hidden;
7+
white-space: nowrap;
8+
text-overflow: ellipsis;
9+
word-break: keep-all;
10+
}
11+
span {
12+
display: inline-block;
13+
white-space: nowrap;
14+
text-overflow: ellipsis;
15+
word-break: keep-all;
16+
}
17+
`;
18+
419
const ColumnTypeViewWrapper = styled.div<{
520
$textOverflow?: boolean
621
}>`
722
position: relative;
8-
${props => props.$textOverflow == false && `
9-
div {
10-
overflow: hidden;
11-
white-space: nowrap;
12-
text-overflow: ellipsis;
13-
word-break: keep-all;
14-
}
15-
span {
16-
display: inline-block; /* Change display to inline-block for span */
17-
white-space: nowrap;
18-
text-overflow: ellipsis;
19-
word-break: keep-all;
20-
}
21-
`}
23+
${props => props.$textOverflow === false && overflowStyles}
2224
`;
2325

2426
const ColumnTypeHoverView = styled.div<{
@@ -89,12 +91,31 @@ function ColumnTypeView(props: {
8991
width?: number;
9092
}>({ done: false });
9193

92-
// Use ref for timeout to avoid state updates
94+
// Use refs for cleanup
9395
const timeoutRef = useRef<NodeJS.Timeout>();
9496
const mountedRef = useRef(true);
97+
const parentElementRef = useRef<HTMLElement | null>(null);
9598

99+
// Cleanup on unmount
100+
useEffect(() => {
101+
return () => {
102+
mountedRef.current = false;
103+
if (timeoutRef.current) {
104+
clearTimeout(timeoutRef.current);
105+
}
106+
if (parentElementRef.current) {
107+
parentElementRef.current.style.zIndex = "";
108+
}
109+
wrapperRef.current = null;
110+
hoverViewRef.current = null;
111+
parentElementRef.current = null;
112+
};
113+
}, []);
114+
115+
// Memoize event handlers
96116
const delayMouseEnter = useCallback(() => {
97-
// Clear any existing timeout
117+
if (!mountedRef.current) return;
118+
98119
if (timeoutRef.current) {
99120
clearTimeout(timeoutRef.current);
100121
}
@@ -107,22 +128,23 @@ function ColumnTypeView(props: {
107128
}, []);
108129

109130
const handleMouseLeave = useCallback(() => {
131+
if (!mountedRef.current) return;
132+
110133
if (timeoutRef.current) {
111134
clearTimeout(timeoutRef.current);
112135
}
113-
if (mountedRef.current) {
114-
setIsHover(false);
115-
}
136+
setIsHover(false);
116137
}, []);
117138

118139
const handleMouseEnter = useCallback(() => {
119-
if (mountedRef.current) {
120-
setIsHover(true);
121-
}
140+
if (!mountedRef.current) return;
141+
setIsHover(true);
122142
}, []);
123143

124144
// Check for overflow
125145
useEffect(() => {
146+
if (!mountedRef.current) return;
147+
126148
const wrapperEle = wrapperRef.current;
127149
if (!isHover || !wrapperEle) {
128150
return;
@@ -133,40 +155,42 @@ function ColumnTypeView(props: {
133155
wrapperEle.clientWidth < wrapperEle.scrollWidth;
134156

135157
if (overflow || childIsOverflow(wrapperEle.children)) {
136-
if (mountedRef.current && !hasOverflow) {
158+
if (!hasOverflow) {
137159
setHasOverflow(true);
138160
}
139-
} else if (mountedRef.current && hasOverflow) {
161+
} else if (hasOverflow) {
140162
setHasOverflow(false);
141163
}
142164
}, [isHover, hasOverflow]);
143165

144166
// Adjust position
145167
useEffect(() => {
168+
if (!mountedRef.current) return;
169+
146170
const wrapperEle = wrapperRef.current;
147171
const hoverEle = hoverViewRef.current;
148172

149173
if (!isHover || !hasOverflow) {
150-
if (wrapperEle?.parentElement) {
151-
wrapperEle.parentElement.style.zIndex = "";
152-
}
153-
if (mountedRef.current) {
154-
setAdjustedPosition({ done: false });
174+
if (parentElementRef.current) {
175+
parentElementRef.current.style.zIndex = "";
155176
}
177+
setAdjustedPosition({ done: false });
156178
return;
157179
}
180+
158181
// Get the position of the outer table
159-
const tableEle = wrapperRef.current?.closest(".ant-table-content") as HTMLDivElement;
182+
const tableEle = wrapperEle?.closest(".ant-table-content") as HTMLDivElement;
160183
if (!hoverEle || !tableEle || !wrapperEle) {
161184
return;
162185
}
163186

187+
// Store parent element reference for cleanup
164188
if (wrapperEle.parentElement) {
165-
// change parent z-index, fix bug when column sticky
166-
wrapperEle.parentElement.style.zIndex = "999";
189+
parentElementRef.current = wrapperEle.parentElement;
190+
parentElementRef.current.style.zIndex = "999";
167191
}
168192

169-
// actual width and height of the element
193+
// Calculate dimensions
170194
const width = Math.min(
171195
hoverEle.getBoundingClientRect().width,
172196
tableEle.getBoundingClientRect().width
@@ -176,13 +200,14 @@ function ColumnTypeView(props: {
176200
tableEle.getBoundingClientRect().height
177201
);
178202

179-
let left;
203+
// Calculate position adjustments
180204
const leftOverflow = tableEle.getBoundingClientRect().x - hoverEle.getBoundingClientRect().x;
181205
const rightOverflow =
182206
tableEle.getBoundingClientRect().x +
183207
tableEle.offsetWidth -
184208
(hoverEle.getBoundingClientRect().x + width);
185209

210+
let left;
186211
if (leftOverflow > 0) {
187212
left = leftOverflow;
188213
} else if (rightOverflow < 0) {
@@ -195,27 +220,17 @@ function ColumnTypeView(props: {
195220
tableEle.offsetHeight -
196221
(hoverEle.getBoundingClientRect().y + height);
197222

198-
// Adjust the hover position according to the table position
199-
if (mountedRef.current) {
200-
setAdjustedPosition({
201-
left,
202-
top: bottomOverflow < 0 ? bottomOverflow : undefined,
203-
height,
204-
width,
205-
done: true,
206-
});
207-
}
223+
setAdjustedPosition({
224+
left,
225+
top: bottomOverflow < 0 ? bottomOverflow : undefined,
226+
height,
227+
width,
228+
done: true,
229+
});
208230
}, [isHover, hasOverflow]);
209231

210-
// Cleanup on unmount
211-
useEffect(() => {
212-
return () => {
213-
mountedRef.current = false;
214-
if (timeoutRef.current) {
215-
clearTimeout(timeoutRef.current);
216-
}
217-
};
218-
}, []);
232+
// Memoize children to prevent unnecessary re-renders
233+
const memoizedChildren = useMemo(() => props.children, [props.children]);
219234

220235
return (
221236
<>
@@ -225,7 +240,7 @@ function ColumnTypeView(props: {
225240
onMouseEnter={delayMouseEnter}
226241
onMouseLeave={handleMouseLeave}
227242
>
228-
{props.children}
243+
{memoizedChildren}
229244
</ColumnTypeViewWrapper>
230245
{isHover && hasOverflow && wrapperRef.current && !props.textOverflow && (
231246
<ColumnTypeHoverView
@@ -240,7 +255,7 @@ function ColumnTypeView(props: {
240255
onMouseEnter={handleMouseEnter}
241256
onMouseLeave={handleMouseLeave}
242257
>
243-
{props.children}
258+
{memoizedChildren}
244259
</ColumnTypeHoverView>
245260
)}
246261
</>

‎client/packages/lowcoder/src/comps/comps/buttonComp/buttonComp.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from "./buttonCompConstants";
2525
import { RefControl } from "comps/controls/refControl";
2626
import { Tooltip } from "antd";
27-
import React, { useContext, useEffect, useCallback } from "react";
27+
import React, { useContext, useEffect, useCallback, useRef } from "react";
2828
import { AnimationStyle } from "@lowcoder-ee/comps/controls/styleControlConstants";
2929
import { styleControl } from "@lowcoder-ee/comps/controls/styleControl";
3030
import { RecordConstructorToComp } from "lowcoder-core";
@@ -178,9 +178,26 @@ const ButtonPropertyView = React.memo((props: {
178178

179179
const ButtonView = React.memo((props: ToViewReturn<ChildrenType>) => {
180180
const editorState = useContext(EditorContext);
181+
const mountedRef = useRef<boolean>(true);
182+
183+
useEffect(() => {
184+
return () => {
185+
mountedRef.current = false;
186+
};
187+
}, []);
181188

182189
const handleClick = useCallback(() => {
183-
isDefault(props.type) ? props.onEvent("click") : submitForm(editorState, props.form);
190+
if (!mountedRef.current) return;
191+
192+
try {
193+
if (isDefault(props.type)) {
194+
props.onEvent("click");
195+
} else {
196+
submitForm(editorState, props.form);
197+
}
198+
} catch (error) {
199+
console.error("Error in button click handler:", error);
200+
}
184201
}, [props.type, props.onEvent, props.form, editorState]);
185202

186203
return (
@@ -208,7 +225,7 @@ const ButtonView = React.memo((props: ToViewReturn<ChildrenType>) => {
208225
)}
209226
</EditorContext.Consumer>
210227
</ButtonCompWrapper>
211-
)
228+
);
212229
});
213230

214231
const buttonViewFn = (props: ToViewReturn<ChildrenType>) => <ButtonView {...props} />

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComp.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,18 @@ export class ColumnTypeComp extends TypedColumnTypeComp {
141141
};
142142
}
143143

144+
private handleTypeChange: (value: ColumnTypeKeys) => void = (value) => {
145+
// Keep the previous text value, some components do not have text, the default value is currentCell
146+
let textRawData = "{{currentCell}}";
147+
if (this.children.comp.children.hasOwnProperty("text")) {
148+
textRawData = (this.children.comp.children as any).text.toJsonValue();
149+
}
150+
this.dispatchChangeValueAction({
151+
compType: value,
152+
comp: { text: textRawData },
153+
} as any);
154+
}
155+
144156
override getPropertyView() {
145157
return (
146158
<>
@@ -149,17 +161,7 @@ export class ColumnTypeComp extends TypedColumnTypeComp {
149161
value={this.children.compType.getView()}
150162
options={actionOptions}
151163
label={trans("table.columnType")}
152-
onChange={(value) => {
153-
// Keep the previous text value, some components do not have text, the default value is currentCell
154-
let textRawData = "{{currentCell}}";
155-
if (this.children.comp.children.hasOwnProperty("text")) {
156-
textRawData = (this.children.comp.children as any).text.toJsonValue();
157-
}
158-
this.dispatchChangeValueAction({
159-
compType: value,
160-
comp: { text: textRawData },
161-
} as any);
162-
}}
164+
onChange={this.handleTypeChange}
163165
/>
164166
{this.children.comp.getPropertyView()}
165167
</>

‎client/packages/lowcoder/src/comps/comps/tableComp/column/columnTypeComps/simpleTextComp.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ const SimpleTextPropertyView = React.memo(({ children }: { children: RecordConst
7676
), [children.text, children.prefixIcon, children.suffixIcon]);
7777
});
7878

79-
export const SimpleTextComp = (function () {
80-
return new ColumnTypeCompBuilder(
79+
export const SimpleTextComp = new ColumnTypeCompBuilder(
8180
childrenMap,
8281
(props, dispatch) => {
8382
const value = props.changeValue ?? getBaseValue(props, dispatch);
@@ -95,4 +94,3 @@ export const SimpleTextComp = (function () {
9594
.setEditViewFn((props) => <SimpleTextEditView {...props} />)
9695
.setPropertyViewFn((children) => <SimpleTextPropertyView children={children} />)
9796
.build();
98-
})();

‎client/packages/lowcoder/src/comps/comps/tableComp/column/tableColumnComp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export const columnChildrenMap = {
146146
fontFamily: withDefault(StringControl, "sans-serif"),
147147
fontStyle: withDefault(StringControl, 'normal'),
148148
cellColor: CellColorComp,
149-
textOverflow: withDefault(TextOverflowControl, "ellipsis"),
149+
textOverflow: withDefault(TextOverflowControl, "wrap"),
150150
linkColor: withDefault(ColorControl, "#3377ff"),
151151
linkHoverColor: withDefault(ColorControl, ""),
152152
linkActiveColor: withDefault(ColorControl, ""),

‎client/packages/lowcoder/src/comps/comps/tableComp/column/tableSummaryColumnComp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const columnChildrenMap = {
4040
fontFamily: withDefault(StringControl, "sans-serif"),
4141
fontStyle: withDefault(StringControl, 'normal'),
4242
cellColor: StringControl,
43-
textOverflow: withDefault(TextOverflowControl, "ellipsis"),
43+
textOverflow: withDefault(TextOverflowControl, "wrap"),
4444
linkColor: withDefault(ColorControl, "#3377ff"),
4545
linkHoverColor: withDefault(ColorControl, ""),
4646
linkActiveColor: withDefault(ColorControl, ""),

‎client/packages/lowcoder/src/comps/comps/tableComp/tableUtils.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ export function columnsToAntdFormat(
396396
}),
397397
editMode,
398398
onTableEvent,
399+
cellIndex: `${column.dataIndex}-${index}`,
399400
});
400401
},
401402
...(column.sortable

0 commit comments

Comments
 (0)
Please sign in to comment.