feat: avoid the unnecessary render when the selected component changes

This commit is contained in:
MrWindlike 2022-05-13 16:30:12 +08:00
parent c6c5fae8b2
commit 7c463749e7
3 changed files with 115 additions and 77 deletions

View File

@ -6,26 +6,43 @@ import { DropComponentWrapper } from './DropComponentWrapper';
import { ChildrenMap } from './StructureTree';
import { genOperation } from '../../operations';
import { EditorServices } from '../../types';
import { observer } from 'mobx-react-lite';
type Props = {
component: ComponentSchema;
parentId: string | undefined;
slot: string | undefined;
childrenMap: ChildrenMap;
selectedComponentId: string;
onSelectComponent: (id: string) => void;
services: EditorServices;
isAncestorDragging: boolean;
depth: number;
};
type ComponentTreeProps = Props & {
isSelected: boolean;
};
export const ComponentTree: React.FC<Props> = props => {
const observeSelected = (Component: React.FC<ComponentTreeProps>) => {
const ObserveActive: React.FC<Props> = props => {
const { services } = props;
const { editorStore } = services;
const { selectedComponentId } = editorStore;
return (
<Component {...props} isSelected={selectedComponentId === props.component.id} />
);
};
return observer(ObserveActive);
};
const ComponentTree = (props: ComponentTreeProps) => {
const {
component,
childrenMap,
parentId,
slot,
selectedComponentId,
isSelected,
onSelectComponent,
services,
isAncestorDragging,
@ -47,13 +64,12 @@ export const ComponentTree: React.FC<Props> = props => {
if (slotChildren && slotChildren.length > 0) {
slotContent = slotChildren.map(c => {
return (
<ComponentTree
<ComponentTreeWrapper
key={c.id}
component={c}
parentId={component.id}
slot={_slot}
childrenMap={childrenMap}
selectedComponentId={selectedComponentId}
onSelectComponent={onSelectComponent}
services={services}
isAncestorDragging={isAncestorDragging || isDragging}
@ -89,7 +105,7 @@ export const ComponentTree: React.FC<Props> = props => {
return (
<Box key={_slot} paddingLeft="3" width="full">
{/* although component can have multiple slots, but for now, most components have only one slot
so we hide slot name to save more view area */}
so we hide slot name to save more view area */}
{slots.length > 1 ? slotName : undefined}
<VStack spacing="0" width="full" alignItems="start">
{slotContent}
@ -101,7 +117,6 @@ export const ComponentTree: React.FC<Props> = props => {
slots,
childrenMap,
component.id,
selectedComponentId,
onSelectComponent,
services,
isAncestorDragging,
@ -146,7 +161,7 @@ export const ComponentTree: React.FC<Props> = props => {
<ComponentItemView
id={component.id}
title={component.id}
isSelected={component.id === selectedComponentId}
isSelected={isSelected}
onClick={onClickItem}
onClickRemove={onClickRemove}
noChevron={slots.length === 0}
@ -161,3 +176,11 @@ export const ComponentTree: React.FC<Props> = props => {
</VStack>
);
};
export const ComponentTreeWrapper: React.FC<Props> = observeSelected(
React.memo(ComponentTree, (prevProps, nextProps) => {
return (Object.keys(prevProps) as (keyof typeof prevProps)[]).every(
key => prevProps[key] === nextProps[key]
);
})
);

View File

@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react';
import React, { useMemo, useRef, useState } from 'react';
import React, { useMemo, useRef, useState, useCallback } from 'react';
import { genOperation } from '../../operations';
import { EditorServices } from '../../types';
@ -31,83 +31,99 @@ export const DropComponentWrapper: React.FC<Props> = props => {
const [dragDirection, setDragDirection] = useState<'prev' | 'next' | undefined>();
const [isDragOver, setIsDragOver] = useState<boolean>(false);
const onDragOver = (e: React.DragEvent) => {
if (!droppable) {
return;
}
setIsDragOver(true);
e.preventDefault();
e.stopPropagation();
const onDragOver = useCallback(
(e: React.DragEvent) => {
if (!droppable) {
return;
}
setIsDragOver(true);
e.preventDefault();
e.stopPropagation();
if (isDropInOnly) return;
if (isDropInOnly) return;
const rect = ref.current?.getBoundingClientRect();
const rect = ref.current?.getBoundingClientRect();
if (!rect) return;
if (!rect) return;
if (e.clientY < rect.top + rect.height / 2) {
setDragDirection('prev');
} else if (e.clientY >= rect.top + rect.height / 2) {
setDragDirection('next');
}
};
if (e.clientY < rect.top + rect.height / 2) {
setDragDirection('prev');
} else if (e.clientY >= rect.top + rect.height / 2) {
setDragDirection('next');
}
},
[droppable, isDropInOnly]
);
const onDragLeave = () => {
const onDragLeave = useCallback(() => {
if (!droppable) {
return;
}
setDragDirection(undefined);
setIsDragOver(false);
};
}, [droppable]);
const onDrop = (e: React.DragEvent) => {
if (!droppable) {
return;
}
e.stopPropagation();
e.preventDefault();
const creatingComponent = e.dataTransfer?.getData('component') || '';
const movingComponent = e.dataTransfer?.getData('moveComponent') || '';
const onDrop = useCallback(
(e: React.DragEvent) => {
if (!droppable) {
return;
}
e.stopPropagation();
e.preventDefault();
const creatingComponent = e.dataTransfer?.getData('component') || '';
const movingComponent = e.dataTransfer?.getData('moveComponent') || '';
let targetParentId = parentId;
let targetParentSlot = parentSlot;
let targetId = componentId;
if (dragDirection === 'next' && isExpanded && hasSlot) {
targetParentId = componentId;
targetParentSlot = 'content';
targetId = undefined;
}
let targetParentId = parentId;
let targetParentSlot = parentSlot;
let targetId = componentId;
if (dragDirection === 'next' && isExpanded && hasSlot) {
targetParentId = componentId;
targetParentSlot = 'content';
targetId = undefined;
}
// move component before or after currentComponent
if (movingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'moveComponent', {
fromId: movingComponent,
toId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
// move component before or after currentComponent
if (movingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'moveComponent', {
fromId: movingComponent,
toId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
// create component as children
if (creatingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'createComponent', {
componentType: creatingComponent,
parentId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
setDragDirection(undefined);
setIsDragOver(false);
};
// create component as children
if (creatingComponent) {
eventBus.send(
'operation',
genOperation(registry, 'createComponent', {
componentType: creatingComponent,
parentId: targetParentId,
slot: targetParentSlot,
targetId: targetId,
direction: dragDirection,
})
);
}
setDragDirection(undefined);
setIsDragOver(false);
},
[
droppable,
dragDirection,
isExpanded,
hasSlot,
componentId,
parentId,
parentSlot,
eventBus,
registry,
]
);
const boxShadow = useMemo(() => {
if (isDropInOnly) return '';
@ -124,7 +140,7 @@ export const DropComponentWrapper: React.FC<Props> = props => {
ref={ref}
width="full"
boxShadow={boxShadow}
background={ isDragOver ? '#ffc6c6' : undefined}
background={isDragOver ? '#ffc6c6' : undefined}
onDrop={onDrop}
onDragOver={onDragOver}
onDragLeave={onDragLeave}

View File

@ -1,7 +1,7 @@
import React, { useMemo, useRef, useEffect } from 'react';
import { ComponentSchema } from '@sunmao-ui/core';
import { Box, Text, VStack } from '@chakra-ui/react';
import { ComponentTree } from './ComponentTree';
import { ComponentTreeWrapper } from './ComponentTree';
import { DropComponentWrapper } from './DropComponentWrapper';
import { resolveApplicationComponents } from '../../utils/resolveApplicationComponents';
import ErrorBoundary from '../ErrorBoundary';
@ -33,20 +33,19 @@ export const StructureTree: React.FC<Props> = props => {
resolveApplicationComponents(realComponents);
return topLevelComponents.map(c => (
<ComponentTree
<ComponentTreeWrapper
key={c.id}
component={c}
parentId={undefined}
slot={undefined}
childrenMap={childrenMap}
selectedComponentId={selectedComponentId}
onSelectComponent={onSelectComponent}
services={services}
isAncestorDragging={false}
depth={0}
/>
));
}, [realComponents, selectedComponentId, onSelectComponent, services]);
}, [realComponents, onSelectComponent, services]);
useEffect(() => {
wrapperRef.current