feat: prevent re-render

This commit is contained in:
dextmorgn
2025-04-04 10:15:44 +02:00
parent d2fe1730d9
commit ff4d6d4cd7
5 changed files with 55 additions and 63 deletions

View File

@@ -36,10 +36,10 @@ export default function RootLayout({
return (
<html suppressHydrationWarning lang="en">
<head>
{/* <script
<script
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
/> */}
/>
</head>
<body
className={clsx(

View File

@@ -60,7 +60,7 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
.insert(dataToInsert)
.select("*")
.single()
console.log(insertError)
console.log(insertError)
if (insertError) {
toast.error("Failed to create node.")
setLoading(false)
@@ -131,7 +131,7 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
const middleIndex = Math.ceil(items.length / 2);
const column1 = items.slice(0, middleIndex);
const column2 = items.slice(middleIndex);
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4 min-w-[400px]">
<div className="space-y-1">

View File

@@ -58,12 +58,9 @@ import {
} from "@/components/ui/resizable"
import NodesPanel from "./nodes-panel"
// Définir les types de nodes et edges à l'extérieur du composant
// pour éviter leur recréation à chaque rendu
const edgeTypes = {
custom: CustomEdge,
}
const nodeTypes = {
individual: IndividualNode,
phone: PhoneNode,
@@ -74,13 +71,10 @@ const nodeTypes = {
group: GroupNode,
default: BaseNode
}
// Séparation claire des sélecteurs pour un contrôle fin des re-rendus
const nodeEdgeSelector = (store: { nodes: any; edges: any }) => ({
nodes: store.nodes,
edges: store.edges,
})
const actionsSelector = (store: {
onNodesChange: any
onEdgesChange: any
@@ -96,7 +90,6 @@ const actionsSelector = (store: {
onPaneClick: store.onPaneClick,
onLayout: store.onLayout,
})
const stateSelector = (store: {
currentNode: any;
setCurrentNode: any,
@@ -112,8 +105,6 @@ const stateSelector = (store: {
updateNode: store.updateNode,
highlightPath: store.highlightPath
})
// Définition de l'interface pour FlowControls
interface FlowControlsProps {
onLayout: (direction: string, fitView: () => void) => void
fitView: () => void
@@ -125,8 +116,6 @@ interface FlowControlsProps {
addNodes: (payload: any) => void
currentNode: any | null | undefined
}
// Mémorisation du composant FlowControls
const FlowControls = memo(
({ onLayout, fitView, handleRefetch, reloading, setView, zoomIn, zoomOut, addNodes, currentNode }: FlowControlsProps) => {
return (
@@ -196,19 +185,15 @@ const FlowControls = memo(
)
}
)
interface LayoutFlowProps {
refetch: () => void
theme: string
}
const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
const { fitView, zoomIn, zoomOut, addNodes, getNode, setCenter, getNodes } = useReactFlow()
const { investigation_id } = useParams()
const { settings } = useInvestigationStore()
const [_, setView] = useQueryState("view", { defaultValue: "flow-graph" })
// État pour le menu contextuel des nœuds
const [nodeContextMenu, setNodeContextMenu] = useState<{
x: number
y: number
@@ -216,8 +201,6 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
nodeType: string | null
data: any
} | null>(null)
// Séparation des selectors pour optimiser les re-rendus
const { nodes, edges } = useFlowStore(nodeEdgeSelector, shallow)
const { onNodesChange, onEdgesChange, onConnect, onNodeClick, onPaneClick, onLayout } = useFlowStore(
actionsSelector,
@@ -231,8 +214,6 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
updateNode,
highlightPath
} = useFlowStore(stateSelector, shallow)
// Initial layout avec useCallback pour éviter les recréations
const initialLayout = useCallback(() => {
const timer = setTimeout(() => {
onLayout("TB", fitView)
@@ -240,53 +221,35 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
}, 500)
return () => clearTimeout(timer)
}, [onLayout, fitView])
// Utiliser l'effet initial avec la fonction mémorisée
useEffect(() => {
return initialLayout()
}, [initialLayout])
// Mémorisation du callback de refetch
const handleRefetch = useCallback(() => {
refetch()
onLayout("TB", fitView)
fitView()
}, [refetch, onLayout, fitView])
// Effet pour gérer la mise en évidence du nœud courant
useEffect(() => {
if (!currentNode) return
const internalNode = getNode(currentNode.id)
if (!internalNode) return
// Utiliser les fonctions du store récupérées via le sélecteur
updateNode(internalNode.id, {
...internalNode,
zIndex: 5000,
data: { ...internalNode.data, forceToolbarVisible: true },
style: { ...internalNode.style, opacity: 1 },
})
const nodeWidth = internalNode.measured?.width ?? 0
const nodeHeight = internalNode.measured?.height ?? 0
setCenter(internalNode.position.x + nodeWidth / 2, internalNode.position.y + nodeHeight / 2 + 20, {
duration: 1000,
zoom: 1.5,
})
// Utiliser highlightPath depuis le sélecteur
// highlightPath(internalNode)
}, [currentNode, getNode, setCenter, updateNode, highlightPath])
// Mémorisation du gestionnaire de connexion
const handleConnect = useCallback(
(params: any) => onConnect(params, investigation_id),
[onConnect, investigation_id]
)
// Gestionnaire optimisé pour le menu contextuel des nœuds
const handleNodeContextMenu = useCallback<NodeMouseHandler>(
(event, node) => {
event.preventDefault()
@@ -302,13 +265,9 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
},
[setCurrentNode]
)
// Fermeture du menu contextuel
const closeNodeContextMenu = useCallback(() => {
setNodeContextMenu(null)
}, [])
// Gestion des clics sur le panneau
const handlePaneClick = useCallback(
(event: React.MouseEvent) => {
closeNodeContextMenu()
@@ -316,8 +275,6 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
},
[onPaneClick, closeNodeContextMenu]
)
// Mémorisation de ReactFlow props pour éviter les recréations
const reactFlowProps = useMemo(() => ({
colorMode: theme as ColorMode,
nodes,
@@ -345,7 +302,8 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
handlePaneClick,
handleNodeContextMenu
])
const processedNodes = useMemo(() => nodes.map(({ id, data, type }: any) => ({ id, data, type })).sort((a: { type: string }, b: { type: any }) => b.type.localeCompare(a.type))
, [nodes.length]);
return (
<ResizablePanelGroup direction="horizontal" className="w-screen grow relative overflow-hidden">
<ResizablePanel defaultSize={80}>
@@ -395,7 +353,7 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
<ResizablePanel defaultSize={20} className="h-full">
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={80}>
<NodesPanel nodes={nodes} />
<NodesPanel nodes={processedNodes} />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={20}>
@@ -406,32 +364,22 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
</ResizablePanelGroup>
)
}
// Mémorisation de LayoutFlow avec un comparateur personnalisé
const MemoizedLayoutFlow = memo(LayoutFlow, (prevProps, nextProps) => {
return prevProps.theme === nextProps.theme && prevProps.refetch === nextProps.refetch
})
// Composant Graph principal
function Graph({ graphQuery }: { graphQuery: any }) {
const [mounted, setMounted] = useState(false)
const { refetch, isLoading, data } = graphQuery
const { resolvedTheme } = useTheme()
// Effet pour initialiser l'état monté
useEffect(() => {
setMounted(true)
}, [])
// Effet pour mettre à jour le store avec de nouvelles données
useEffect(() => {
if (data) {
useFlowStore.setState({ nodes: data?.nodes, edges: data?.edges })
setMounted(true)
}
}, [data])
// Rendu conditionnel basé sur l'état de chargement
if (!mounted || isLoading) {
return (
<div className="grow w-full flex items-center justify-center">
@@ -439,13 +387,10 @@ function Graph({ graphQuery }: { graphQuery: any }) {
</div>
)
}
return (
<ReactFlowProvider>
<MemoizedLayoutFlow refetch={refetch} theme={resolvedTheme || "light"} />
</ReactFlowProvider>
)
}
// Export du composant Graph mémorisé
export default memo(Graph)

View File

@@ -4,6 +4,7 @@ import { useFlowStore } from '@/store/flow-store'
import { Button } from '@/components/ui/button'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { TypeBadge } from '@/components/type-badge'
const NodesPanel = ({ nodes }: { nodes: Node[] }) => {
const { setCurrentNode } = useFlowStore()
return (
@@ -15,7 +16,7 @@ const NodesPanel = ({ nodes }: { nodes: Node[] }) => {
<AvatarFallback>{node?.data?.full_name?.[0]}</AvatarFallback>
</Avatar>
<div className='grow truncate text-ellipsis'>{node?.data?.full_name || node?.data?.label}</div>
<Badge variant={"outline"}>{node?.type}</Badge>
<TypeBadge type={node?.type}></TypeBadge>
</Button>
))}
</div>

View File

@@ -0,0 +1,46 @@
import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
import { memo } from "react"
type TypeBadgeProps = {
type: string
className?: string
}
const typeColorMap: Record<string, string> = {
individual: "bg-emerald-100 text-emerald-800 hover:bg-emerald-100/80 dark:bg-emerald-900/30 dark:text-emerald-300 dark:hover:bg-emerald-900/40",
phone: "bg-sky-100 text-sky-800 hover:bg-sky-100/80 dark:bg-sky-900/30 dark:text-sky-300 dark:hover:bg-sky-900/40",
address: "bg-amber-100 text-amber-800 hover:bg-amber-100/80 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/40",
email: "bg-violet-100 text-violet-800 hover:bg-violet-100/80 dark:bg-violet-900/30 dark:text-violet-300 dark:hover:bg-violet-900/40",
ip: "bg-slate-100 text-slate-800 hover:bg-slate-100/80 dark:bg-slate-800/50 dark:text-slate-300 dark:hover:bg-slate-800/60",
social_account: "bg-pink-100 text-pink-800 hover:bg-pink-100/80 dark:bg-pink-900/30 dark:text-pink-300 dark:hover:bg-pink-900/40",
vehicle: "bg-cyan-100 text-cyan-800 hover:bg-cyan-100/80 dark:bg-cyan-900/30 dark:text-cyan-300 dark:hover:bg-cyan-900/40",
organization: "bg-orange-100 text-orange-800 hover:bg-orange-100/80 dark:bg-orange-900/30 dark:text-orange-300 dark:hover:bg-orange-900/40",
website: "bg-purple-100 text-purple-800 hover:bg-purple-100/80 dark:bg-purple-900/30 dark:text-purple-300 dark:hover:bg-purple-900/40",
document: "bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80 dark:bg-yellow-900/30 dark:text-yellow-300 dark:hover:bg-yellow-900/40",
financial: "bg-green-100 text-green-800 hover:bg-green-100/80 dark:bg-green-900/30 dark:text-green-300 dark:hover:bg-green-900/40",
event: "bg-red-100 text-red-800 hover:bg-red-100/80 dark:bg-red-900/30 dark:text-red-300 dark:hover:bg-red-900/40",
device: "bg-blue-100 text-blue-800 hover:bg-blue-100/80 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-900/40",
media: "bg-fuchsia-100 text-fuchsia-800 hover:bg-fuchsia-100/80 dark:bg-fuchsia-900/30 dark:text-fuchsia-300 dark:hover:bg-fuchsia-900/40",
education: "bg-teal-100 text-teal-800 hover:bg-teal-100/80 dark:bg-teal-900/30 dark:text-teal-300 dark:hover:bg-teal-900/40",
relationship: "bg-rose-100 text-rose-800 hover:bg-rose-100/80 dark:bg-rose-900/30 dark:text-rose-300 dark:hover:bg-rose-900/40",
online_activity: "bg-indigo-100 text-indigo-800 hover:bg-indigo-100/80 dark:bg-indigo-900/30 dark:text-indigo-300 dark:hover:bg-indigo-900/40",
digital_footprint: "bg-lime-100 text-lime-800 hover:bg-lime-100/80 dark:bg-lime-900/30 dark:text-lime-300 dark:hover:bg-lime-900/40",
biometric: "bg-amber-100 text-amber-800 hover:bg-amber-100/80 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/40",
credential: "bg-gray-100 text-gray-800 hover:bg-gray-100/80 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800/60",
}
function TypeBadgeComponent({ type, className }: TypeBadgeProps) {
const colorClasses = typeColorMap[type] || "bg-gray-100 text-gray-800 hover:bg-gray-100/80 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800/60"
return (
<Badge
variant="outline"
className={cn(colorClasses, "font-medium border-transparent", className)}
>
{type}
</Badge>
)
}
export const TypeBadge = memo(TypeBadgeComponent)