Redeuce all unnecessary table re-rendering

This commit is contained in:
KarimTamani
2025-05-22 18:08:38 +01:00
parent 2d5a082210
commit 33c8047ea1
15 changed files with 229 additions and 123 deletions
+1 -4
View File
@@ -19,7 +19,7 @@ const useGetOverlappingNodes = (): Set<string> => {
}
}
return overlaps
}, [nodes]) ;
}, [nodes]);
return overlappingNodes;
}
@@ -48,9 +48,6 @@ function isNodesOverlapping(nodeA: Node, nodeB: Node) {
bottom: nodeB.position.y + nodeBHeight,
};
return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
}
+13 -7
View File
@@ -1,4 +1,4 @@
import { Edge, useStore } from "@xyflow/react";
import { Edge, useReactFlow, useStore } from "@xyflow/react";
import { useEffect, useState } from "react";
import hash from 'object-hash';
@@ -6,19 +6,25 @@ import hash from 'object-hash';
const useGetRelatedEdges = ( nodeId : string) => {
const edges = useStore((store) => store.edges) as Edge[];
const useGetRelatedEdges = (nodeId: string) => {
const [relatedEdges, setRelatedEdges] = useState<Edge[]>([]);
const { getEdges } = useReactFlow();
const edges = getEdges();
useEffect(() => {
const newRelatedEdges: Edge[] = edges.filter((edge: Edge) => edge.source == nodeId || edge.target == nodeId);
setRelatedEdges((previousEdges :Edge[]) => {
return hash(newRelatedEdges) == hash(previousEdges) ? previousEdges : newRelatedEdges
})
if (hash(newRelatedEdges) != hash(relatedEdges)) {
setRelatedEdges(newRelatedEdges)
}
}, [edges, nodeId])
return relatedEdges
}
}
export default useGetRelatedEdges;
+2
View File
@@ -23,7 +23,9 @@ export const useRelationshipToEdge = (relationships: RelationshipType[]): void =
} as Edge
})
setEdges((edges: any) => {
return relationshipEdges.map((relationshipEdge: any) => {
const edge = edges.find((edge: Edge) => edge.id == relationshipEdge.id);
if (!edge)
+9 -19
View File
@@ -1,25 +1,29 @@
import { TableType } from "@/lib/schemas/table-schema";
import { Node, useReactFlow } from "@xyflow/react";
import { useEffect } from "react";
import useGetOverlappingNodes from "./use-get-overlapping-nodes";
import { useEffect } from "react";
import hash from 'object-hash';
import { getDefaultTableOverlapping } from "@/utils/tables";
export const useTableToNode = (tables: TableType[]): void => {
const { setNodes } = useReactFlow();
useEffect(() => {
const tableNodes = tables.map((table: TableType) => {
return {
id: table.id,
type: "table",
position: {
x: table.posX,
y: table.posY
},
data: {
table,
table,
overlapping : getDefaultTableOverlapping(table , tables) ,
pulsing : false ,
highlightedEdges : []
},
style: {
width: 224
@@ -27,21 +31,7 @@ export const useTableToNode = (tables: TableType[]): void => {
} as Node
})
setNodes((nodes) => {
return tableNodes.map((tableNode: Node) => {
const node = nodes.find((node: Node) => node.id == tableNode.id);
if (!node) {
return tableNode;
}
const hashNode: string = hash((node.data as any).table);
const hashTableNode: string = hash((tableNode.data as any).table);
return hashNode == hashTableNode ? node : tableNode;
})
})
setNodes (tableNodes) ;
}, [tables])
}
+31 -13
View File
@@ -14,7 +14,7 @@ import { v4 } from "uuid";
import { TARGET_PREFIX } from "./table/field";
import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker";
import { areArraysEqual } from "@/utils/utils";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { addToast, Button, Image } from "@heroui/react";
import { AlertTriangle, LayoutGrid } from "lucide-react";
@@ -23,17 +23,17 @@ import { useTheme } from "next-themes";
import DatabaseControlButtons from "./database-control-buttons";
import useGetOverlappingNodes from "@/hooks/use-get-overlapping-nodes";
import { FieldType } from "@/lib/schemas/field-schema";
import hash from "object-hash";
const DatabasePage: React.FC<never> = () => {
const { database } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship, getField } = useDatabaseOperations();
const { database, getField } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const { setIsConnectionInProgress } = useDiagram();
const { setIsConnectionInProgress } = useDiagramOps();
const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([]);
const [isTableOverlappingPulsing, setIsTableOverlappingPulsing] = useState<boolean>(false);
const { tables, relationships } = database;
@@ -48,7 +48,8 @@ const DatabasePage: React.FC<never> = () => {
const sourceField: FieldType | undefined = getField(connection.source, sourceFieldId as string);
const targetField: FieldType | undefined = getField(connection.target, targetFieldId);
console.log(sourceField, targetField)
if (sourceField?.typeId == targetField?.typeId) {
createRelationship({
@@ -73,7 +74,11 @@ const DatabasePage: React.FC<never> = () => {
const handleNodesChanges: OnNodesChange<never> = useCallback((changes: NodeChange<never>[]) => {
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) => change.type == "position" && !change.dragging) as NodePositionChange[];
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) =>
change.type == "position" &&
!change.dragging
) as NodePositionChange[];
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
if (nodePositionChanges.length > 0)
@@ -115,15 +120,29 @@ const DatabasePage: React.FC<never> = () => {
useEffect(() => {
setEdges((edges: any) => {
return edges.map((edge: any) => {
const selected: boolean = selectedEdgeIds.includes(edge.id);
return (edge.animated == selected) ? edge : { ...edge, animated: selected } as Edge;
})
});
setNodes((nodes: any) => {
return nodes.map((node: any) => {
const nodeHighlitedEdges: Edge[] = node.data.highlightedEdges;
const newHighlitedEdges: Edge[] = edges.filter((edge: Edge) => selectedEdgeIds.includes(edge.id) && (edge.source == node.id || edge.target == node.id));
console.log(nodeHighlitedEdges, newHighlitedEdges);
return {
...edge,
animated: selected,
} as Edge
...node,
data: {
...node.data,
highlightedEdges: newHighlitedEdges
}
}
})
})
}, [selectedEdgeIds]);
@@ -155,7 +174,6 @@ const DatabasePage: React.FC<never> = () => {
const overlappingNodes = useGetOverlappingNodes();
useEffect(() => {
const overlappingIds = Array.from(overlappingNodes);
@@ -1,7 +1,7 @@
import { useRelationshipName } from "@/hooks/use-relationship-name";
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil, Trash } from "lucide-react";
import { useEffect, useState } from "react";
@@ -23,7 +23,7 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
const { t } = useTranslation();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
const { focusOnRelationship } = useDiagram();
const { focusOnRelationship } = useDiagramOps();
const editRelationshipName = () => {
@@ -43,8 +43,8 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
useEffect(() => {
setName(relationship.name ? relationship.name : defaultName) ;
} , [relationship.name])
setName(relationship.name ? relationship.name : defaultName);
}, [relationship.name])
return (
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
<div className={cn(
@@ -6,10 +6,10 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { getNextSequence } from "@/utils/field";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
export interface TableAccordionHeaderProps {
table: TableType,
@@ -24,17 +24,17 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
const { t } = useTranslation();
const [editMode, setEditMode] = useState<boolean>(false);
const { focusOnTable } = useDiagram();
const { focusOnTable } = useDiagramOps();
useEffect(() => {
useEffect(() => {
setTableName(table.name);
}, [table.name])
const saveTableName = useCallback(async() => {
const saveTableName = useCallback(async () => {
await editTable({ id: table.id, name: tableName } as TableInsertType);
setEditMode(false);
} , [tableName])
}, [tableName])
const onDeleteTable = async () => {
deleteTable(table.id)
@@ -126,7 +126,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
size="sm"
isIconOnly
variant="light"
onPressEnd={() => focusOnTable(table.id , true)}
onPressEnd={() => focusOnTable(table.id, true)}
>
<Focus className="size-4 text-icon dark:text-white" />
</Button>
+2 -2
View File
@@ -1,7 +1,7 @@
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useDiagram, useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { card, cn } from "@heroui/react";
import { Edge, EdgeProps, getBezierPath, getSmoothStepPath, InternalNode, Node, Position, useReactFlow } from "@xyflow/react";
import React, { useMemo } from "react";
@@ -18,7 +18,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
let { id, sourceX, sourceY, targetX, targetY, source, target, selected, data, animated } = props;
const { getInternalNode, getEdge } = useReactFlow();
const { focusOnRelationship } = useDiagram();
const { focusOnRelationship } = useDiagramOps();
const sourceNode = getInternalNode(source);
+26 -18
View File
@@ -1,7 +1,7 @@
import { Edge, Node, NodeProps, useConnection, useStore } from "@xyflow/react";
import hash from 'object-hash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Card, cn, } from "@heroui/react";
import {
@@ -17,22 +17,26 @@ import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useTranslation } from "react-i18next";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import useGetRelatedEdges from "@/hooks/use-get-related-edges";
export type TableProps = Node<{
table: TableType,
overlapping?: boolean,
pulsing?: boolean,
highlightedEdges : Edge[]
}>
const Table: React.FC<NodeProps<TableProps>> = (props) => {
const { selected, data: { table, overlapping = false, pulsing = false } } = props;
const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overlapping = false, pulsing = false , highlightedEdges = [] } }) => {
const [editMode, setEditMode] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
const { editTable } = useDatabaseOperations();
const edges = useGetRelatedEdges(table.id as string);
const { focusOnTable } = useDiagram();
// const edges = useGetRelatedEdges(table.id as string);
const { focusOnTable } = useDiagramOps();
const { t } = useTranslation();
useEffect(() => {
@@ -47,11 +51,13 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
const focus = useCallback(() => {
focusOnTable(table.id, false);
}, [table])
/*
const highlightedEdges: Edge[] = useMemo(() => {
return edges.filter((edge: Edge) => edge.animated || edge.selected);
}, [edges]);
*/
const fields: React.ReactNode[] = useMemo(() => {
return table.fields.map((field: FieldType) => {
const highlight: boolean = highlightedEdges.find((edge: any) =>
@@ -66,25 +72,19 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
/>)
})
}, [table.fields, selected, highlightedEdges]);
useEffect(() => {
console.log("re-render ", table.name)
}, [useDatabase]) ;
return (
console.log ("re-render " , table.name)
return (
<Card className={cn(
"w-full h-full bg-background rounded-lg noselect overflow-visible dark:bg-default-900",
selected
? 'ring-2 ring-primary'
: '',
overlapping
? 'ring-2 dark:ring-offset-default-900 ring-danger ring-offset-1 scale-105 shadow-danger '
: '',
!pulsing
? 'scale-100'
: '',
@@ -166,4 +166,12 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
)
};
export default React.memo(Table)
export default React.memo(Table, (previousState: any, newState: any) => {
const previousStateHash: string = hash(previousState.data);
const newStateHash: string = hash(newState.data);
return previousStateHash == newStateHash && previousState.selected == newState.selected;
})
@@ -15,8 +15,8 @@ interface DatabaseDataContextType {
data_types: DataType[],
database: DatabaseType,
isLoading: boolean,
// table operations
isLoading: boolean,
getField: (tableId: string, id: string) => FieldType | undefined,
}
@@ -33,7 +33,6 @@ interface DatabaseOperationsContextType {
editField: (field: FieldInsertType) => Promise<QueryResult>,
deleteField: (id: string) => Promise<void>,
orderTableFields: (fields: FieldType[]) => Promise<QueryResult>,
getField: (tableId: string, id: string) => FieldType | undefined,
// relationship operations
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
@@ -1,6 +1,6 @@
import { DatabaseDataContext, DatabaseOperationsContext } from "./database-context";
import { useCallback, useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { db, powerSyncDb } from "../sync-provider/sync-provider";
import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
import { useQuery } from "@powersync/react";
@@ -223,38 +223,53 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}
})
}, [db, currentDatabaseId]);
}, [db]);
const databaseOpsValue = useMemo(() => ({
createTable,
editTable,
deleteTable,
updateTablePositions,
deleteMultiTables,
createField,
editField,
deleteField,
orderTableFields,
createRelationship,
editRelationship,
deleteRelationship,
deleteMultiRelationships,
executeDbDiffOps,
}), [
createTable,
editTable,
deleteTable,
updateTablePositions,
deleteMultiTables,
createField,
editField,
deleteField,
orderTableFields,
createRelationship,
editRelationship,
deleteRelationship,
deleteMultiRelationships,
executeDbDiffOps,
]);
return (
<DatabaseDataContext.Provider value={{
data_types,
database: database as unknown as DatabaseType,
isLoading,
getField,
}}>
<DatabaseOperationsContext.Provider value={{
createTable,
editTable,
deleteTable,
updateTablePositions,
deleteMultiTables,
createField,
editField,
deleteField,
orderTableFields,
getField,
createRelationship,
editRelationship,
deleteRelationship,
deleteMultiRelationships,
executeDbDiffOps,
}}>
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
{
!isLoading && database &&
@@ -1,18 +1,18 @@
import { createContext, Dispatch, SetStateAction } from "react";
export interface DiagramContextType {
interface DiagramDataContextType {
focusedTableId: string | undefined;
focusedRelationshipId: string | undefined;
isConnectionInProgress: boolean
}
focusedTableId : string | undefined ;
focusedRelationshipId : string | undefined;
isConnectionInProgress : boolean
focusOnTable : ( id : string , transition? : boolean ) => void ,
focusOnRelationship : ( id : string, transition? : boolean ) => void ,
setIsConnectionInProgress : Dispatch<boolean>
}
interface DiagramOpsContextType {
focusOnTable: (id: string, transition?: boolean) => void,
focusOnRelationship: (id: string, transition?: boolean) => void,
setIsConnectionInProgress: Dispatch<boolean>
}
export default createContext<DiagramContextType>({} as DiagramContextType);
export const DiagramDataContext = createContext<DiagramDataContextType>({} as DiagramDataContextType);
export const DiagramOpsContext = createContext<DiagramOpsContextType>({} as DiagramOpsContextType);
@@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { FitViewOptions, useReactFlow } from "@xyflow/react";
import { useNavigate } from "react-router-dom";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import DiagramContext from "./diagram-context";
import { DiagramDataContext, DiagramOpsContext } from "./diagram-context";
@@ -73,26 +73,35 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
}, [setFocusedRelationshipId]);
const contextValue = useMemo(() => ({
const contextDatatValue = useMemo(() => ({
focusedTableId,
focusedRelationshipId,
isConnectionInProgress,
}), [focusedTableId, focusedRelationshipId, isConnectionInProgress,]);
const contextOpsValues = useMemo(() => ({
focusOnTable,
focusOnRelationship,
setIsConnectionInProgress
}), [focusedTableId, focusedRelationshipId, focusOnTable, focusOnRelationship, isConnectionInProgress, setIsConnectionInProgress]);
}), [focusOnTable, focusOnRelationship, setIsConnectionInProgress])
return (
<DiagramContext.Provider
value={contextValue}
<DiagramDataContext.Provider
value={contextDatatValue}
>
{children}
</DiagramContext.Provider>
<DiagramOpsContext.Provider value={contextOpsValues}>
{children}
</DiagramOpsContext.Provider>
</DiagramDataContext.Provider>
)
}
export const useDiagram = () => useContext(DiagramContext);
export const useDiagram = () => useContext(DiagramDataContext);
export const useDiagramOps = () => useContext(DiagramOpsContext);
export default DiagramProvider;
+37
View File
@@ -51,7 +51,44 @@ const adjustTablesPositions = async (
};
const isTablesOverlapping = (tableA: TableType, tableB: TableType) => {
const tableAWidth: number = 224;
const tableAHeight: number = tableA.fields.length * 32 + 36;
const tableBWidth: number = 224;
const tableBHeight: number = tableB.fields.length * 32 + 36;
const a = {
left: tableA.posX,
right: tableA.posX + tableAWidth,
top: tableA.posY,
bottom: tableA.posY + tableAHeight,
};
const b = {
left: tableB.posX,
right: tableB.posX + tableBWidth,
top: tableB.posY,
bottom: tableB.posY + tableBHeight,
};
return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
}
const getDefaultTableOverlapping = (table: TableType, tables: TableType[]): boolean => {
for (let index: number = 0; index < tables.length; index++) {
if ( table.id == tables[index].id) continue ;
if (isTablesOverlapping(table, tables[index]))
return true;
}
return false;
}
export {
adjustTablesPositions,
getDefaultTableOverlapping
}