Skip to content

Commit a7a6599

Browse files
committed
perf: don't remount slot when parent changes
1 parent d5f934f commit a7a6599

5 files changed

Lines changed: 154 additions & 56 deletions

File tree

packages/core/components/DropZone/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { renderContext } from "../Render";
4646
import { useSlots } from "../../lib/use-slots";
4747
import { ContextSlotRender, SlotRenderPure } from "../SlotRender";
4848
import { expandNode } from "../../lib/data/flatten-node";
49-
import { useFieldTransforms } from "../../lib/field-transforms/use-field-transforms";
49+
import { useFieldTransformsTracked } from "../../lib/field-transforms/use-field-transforms-tracked";
5050
import { getInlineTextTransform } from "../../lib/field-transforms/default-transforms/inline-text-transform";
5151
import { getSlotTransform } from "../../lib/field-transforms/default-transforms/slot-transform";
5252
import { getRichTextTransform } from "../../lib/field-transforms/default-transforms/rich-text-transform";
@@ -214,7 +214,7 @@ const DropZoneChild = ({
214214
[plugins, userFieldTransforms]
215215
);
216216

217-
const transformedProps = useFieldTransforms(
217+
const transformedProps = useFieldTransformsTracked(
218218
config,
219219
defaultedNode,
220220
combinedFieldTransforms,

packages/core/lib/data/map-fields.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,29 +204,34 @@ export function mapFields<T extends ComponentData | RootData>(
204204
item: T,
205205
mappers: Mappers<MapFn>,
206206
config: Config,
207-
recurseSlots?: boolean
207+
recurseSlots?: boolean,
208+
shouldDefaultSlots?: boolean
208209
): T;
209210

210211
export function mapFields<T extends ComponentData | RootData>(
211212
item: T,
212213
mappers: Mappers<PromiseMapFn>,
213214
config: Config,
214-
recurseSlots?: boolean
215+
recurseSlots?: boolean,
216+
shouldDefaultSlots?: boolean
215217
): Promise<T>;
216218

217219
export function mapFields(
218220
item: any,
219221
mappers: Mappers,
220222
config: Config,
221-
recurseSlots: boolean = false
223+
recurseSlots: boolean = false,
224+
shouldDefaultSlots: boolean = true
222225
): any {
223226
const itemType = "type" in item ? item.type : "root";
224227

225228
const componentConfig =
226229
itemType === "root" ? config.root : config.components?.[itemType];
227230

228231
const newProps = walkObject({
229-
value: defaultSlots(item.props ?? {}, componentConfig?.fields ?? {}),
232+
value: shouldDefaultSlots
233+
? defaultSlots(item.props ?? {}, componentConfig?.fields ?? {})
234+
: item.props,
230235
fields: componentConfig?.fields ?? {},
231236
mappers,
232237
id: item.props ? item.props.id ?? "root" : "root",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
ComponentData,
3+
Config,
4+
ExtractField,
5+
Field,
6+
UserGenerics,
7+
} from "../../types";
8+
import { MapFnParams, Mappers } from "../data/map-fields";
9+
import {
10+
FieldTransformFn,
11+
FieldTransforms,
12+
} from "../../types/API/FieldTransforms";
13+
14+
/**
15+
* Converts transformers to mappers
16+
*
17+
* Transformers are the same as mappers, except they receive the additional `isReadOnly` param.
18+
* This converts transformers to mappers by adding the `isReadOnly` param.
19+
*/
20+
export function buildMappers<
21+
T extends ComponentData,
22+
UserConfig extends Config,
23+
G extends UserGenerics<UserConfig>
24+
>(
25+
transforms: FieldTransforms,
26+
readOnly?: T["readOnly"],
27+
forceReadOnly?: boolean
28+
) {
29+
return Object.keys(transforms).reduce<Mappers>((acc, _fieldType) => {
30+
const fieldType = _fieldType as Field["type"]; // Not strictly true, as could include user fields, but this should be safe enough
31+
32+
return {
33+
...acc,
34+
[fieldType]: ({
35+
parentId,
36+
...params
37+
}: MapFnParams<ExtractField<G["UserField"], Field["type"]>>) => {
38+
const wildcardPath = params.propPath.replace(/\[\d+\]/g, "[*]");
39+
40+
const isReadOnly =
41+
readOnly?.[params.propPath] ||
42+
readOnly?.[wildcardPath] ||
43+
forceReadOnly ||
44+
false;
45+
46+
const fn = transforms[fieldType] as FieldTransformFn<
47+
ExtractField<G["UserField"], Field["type"]>
48+
>;
49+
50+
return fn?.({
51+
...params,
52+
isReadOnly,
53+
componentId: parentId,
54+
});
55+
},
56+
};
57+
}, {});
58+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client";
2+
3+
import { ComponentData, Config } from "../../types";
4+
import { useMemo, useRef } from "react";
5+
import { mapFields, Mappers } from "../data/map-fields";
6+
import { FieldTransforms } from "../../types/API/FieldTransforms";
7+
import { buildMappers } from "./build-mappers";
8+
9+
export function useFieldTransformsTracked<
10+
T extends ComponentData,
11+
UserConfig extends Config
12+
>(
13+
config: UserConfig,
14+
item: T,
15+
transforms: FieldTransforms,
16+
readOnly?: T["readOnly"],
17+
forceReadOnly?: boolean
18+
): T["props"] {
19+
const prevProps = useRef<Record<string, any>>(null);
20+
const prevResult = useRef<Record<string, any>>(item.props);
21+
22+
const mappers = useMemo<Mappers>(
23+
() => buildMappers(transforms, readOnly, forceReadOnly),
24+
[transforms, readOnly, forceReadOnly]
25+
);
26+
27+
const transformedProps = useMemo(() => {
28+
// Filter to changed fields only (shallow comparison)
29+
const changedProps: Record<string, any> = {};
30+
31+
const componentConfig =
32+
item.type === "root" ? config.root : config.components?.[item.type];
33+
34+
let changeIncludesSlot = false;
35+
36+
for (const fieldName in item.props) {
37+
const fieldType = componentConfig?.fields?.[fieldName]?.type;
38+
39+
if (
40+
!prevProps.current ||
41+
item.props[fieldName] !== prevProps.current[fieldName]
42+
) {
43+
changedProps[fieldName] = item.props[fieldName];
44+
45+
if (fieldType === "slot") {
46+
changeIncludesSlot = true;
47+
}
48+
}
49+
}
50+
51+
// Always include ID
52+
changedProps.id = item.props.id;
53+
54+
prevProps.current = item.props;
55+
56+
const mapped = mapFields(
57+
{ ...item, props: changedProps },
58+
mappers,
59+
config,
60+
false,
61+
changeIncludesSlot
62+
).props;
63+
64+
prevResult.current = { ...prevResult.current, ...mapped };
65+
66+
return prevResult.current;
67+
}, [config, item, mappers]);
68+
69+
const mergedProps = useMemo(
70+
() => ({ ...item.props, ...transformedProps }),
71+
[item.props, transformedProps]
72+
);
73+
74+
return mergedProps;
75+
}

packages/core/lib/field-transforms/use-field-transforms.tsx

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,26 @@
1-
import {
2-
ComponentData,
3-
Config,
4-
ExtractField,
5-
Field,
6-
UserGenerics,
7-
} from "../../types";
1+
import { ComponentData, Config } from "../../types";
82
import { useMemo } from "react";
9-
import { mapFields, MapFnParams, Mappers } from "../data/map-fields";
10-
import {
11-
FieldTransformFn,
12-
FieldTransforms,
13-
} from "../../types/API/FieldTransforms";
3+
import { mapFields, Mappers } from "../data/map-fields";
4+
import { FieldTransforms } from "../../types/API/FieldTransforms";
5+
import { buildMappers } from "./build-mappers";
146

157
export function useFieldTransforms<
168
T extends ComponentData,
17-
UserConfig extends Config,
18-
G extends UserGenerics<UserConfig>
9+
UserConfig extends Config
1910
>(
2011
config: UserConfig,
2112
item: T,
2213
transforms: FieldTransforms,
2314
readOnly?: T["readOnly"],
2415
forceReadOnly?: boolean
2516
): T["props"] {
26-
// Transformers are the same as mappers, except they receive the additional `isReadOnly` param.
27-
// This converts transformers to mappers by adding the `isReadOnly` param
28-
const mappers = useMemo<Mappers>(() => {
29-
return Object.keys(transforms).reduce<Mappers>((acc, _fieldType) => {
30-
const fieldType = _fieldType as Field["type"]; // Not strictly true, as could include user fields, but this should be safe enough
31-
32-
return {
33-
...acc,
34-
[fieldType]: ({
35-
parentId,
36-
...params
37-
}: MapFnParams<ExtractField<G["UserField"], Field["type"]>>) => {
38-
const wildcardPath = params.propPath.replace(/\[\d+\]/g, "[*]");
39-
40-
const isReadOnly =
41-
readOnly?.[params.propPath] ||
42-
readOnly?.[wildcardPath] ||
43-
forceReadOnly ||
44-
false;
45-
46-
const fn = transforms[fieldType] as FieldTransformFn<
47-
ExtractField<G["UserField"], Field["type"]>
48-
>;
49-
50-
return fn?.({
51-
...params,
52-
isReadOnly,
53-
componentId: parentId,
54-
});
55-
},
56-
};
57-
}, {});
58-
}, [transforms, readOnly, forceReadOnly]);
17+
const mappers = useMemo<Mappers>(
18+
() => buildMappers(transforms, readOnly, forceReadOnly),
19+
[transforms, readOnly, forceReadOnly]
20+
);
5921

6022
const transformedProps = useMemo(() => {
61-
const mapped = mapFields(item, mappers, config).props;
62-
63-
return mapped;
23+
return mapFields(item, mappers, config).props;
6424
}, [config, item, mappers]);
6525

6626
const mergedProps = useMemo(

0 commit comments

Comments
 (0)