2
0

perf(flow): ️ Smooth panning even with complexe flow

This commit is contained in:
Baptiste Arnaud
2022-03-02 12:21:32 +01:00
parent 3c6783727e
commit e9a9dc00e2
6 changed files with 63 additions and 58 deletions

View File

@@ -1,6 +1,5 @@
import { useEventListener } from '@chakra-ui/hooks' import { useEventListener } from '@chakra-ui/hooks'
import assert from 'assert' import assert from 'assert'
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
import { useGraph, ConnectingIds } from 'contexts/GraphContext' import { useGraph, ConnectingIds } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
@@ -18,7 +17,6 @@ export const DrawingEdge = () => {
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
blocksCoordinates, blocksCoordinates,
graphOffsetY,
} = useGraph() } = useGraph()
const { createEdge } = useTypebot() const { createEdge } = useTypebot()
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
@@ -32,19 +30,21 @@ export const DrawingEdge = () => {
if (!connectingIds) return 0 if (!connectingIds) return 0
return getEndpointTopOffset( return getEndpointTopOffset(
sourceEndpoints, sourceEndpoints,
graphOffsetY, graphPosition.y,
connectingIds.source.itemId ?? connectingIds.source.stepId connectingIds.source.itemId ?? connectingIds.source.stepId
) )
}, [connectingIds, sourceEndpoints, graphOffsetY]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [connectingIds, sourceEndpoints])
const targetTop = useMemo(() => { const targetTop = useMemo(() => {
if (!connectingIds) return 0 if (!connectingIds) return 0
return getEndpointTopOffset( return getEndpointTopOffset(
targetEndpoints, targetEndpoints,
graphOffsetY, graphPosition.y,
connectingIds.target?.stepId connectingIds.target?.stepId
) )
}, [connectingIds, targetEndpoints, graphOffsetY]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [connectingIds, targetEndpoints])
const path = useMemo(() => { const path = useMemo(() => {
if (!sourceTop || !sourceBlockCoordinates) return `` if (!sourceTop || !sourceBlockCoordinates) return ``
@@ -72,7 +72,7 @@ export const DrawingEdge = () => {
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
setMousePosition({ setMousePosition({
x: e.clientX - graphPosition.x, x: e.clientX - graphPosition.x,
y: e.clientY - graphPosition.y - headerHeight, y: e.clientY - graphPosition.y,
}) })
} }
useEventListener('mousemove', handleMouseMove) useEventListener('mousemove', handleMouseMove)

View File

@@ -24,7 +24,7 @@ export const DropOffEdge = ({
onUnlockProPlanClick, onUnlockProPlanClick,
}: Props) => { }: Props) => {
const { user } = useUser() const { user } = useUser()
const { sourceEndpoints, blocksCoordinates, graphOffsetY } = useGraph() const { sourceEndpoints, blocksCoordinates, graphPosition } = useGraph()
const { publishedTypebot } = useTypebot() const { publishedTypebot } = useTypebot()
const isUserOnFreePlan = isFreePlan(user) const isUserOnFreePlan = isFreePlan(user)
@@ -60,7 +60,7 @@ export const DropOffEdge = ({
() => () =>
getEndpointTopOffset( getEndpointTopOffset(
sourceEndpoints, sourceEndpoints,
graphOffsetY, graphPosition.y,
block?.steps[block.steps.length - 1].id block?.steps[block.steps.length - 1].id
), ),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -25,7 +25,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
blocksCoordinates, blocksCoordinates,
graphOffsetY, graphPosition,
} = useGraph() } = useGraph()
const [isMouseOver, setIsMouseOver] = useState(false) const [isMouseOver, setIsMouseOver] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
@@ -42,7 +42,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
() => () =>
getEndpointTopOffset( getEndpointTopOffset(
sourceEndpoints, sourceEndpoints,
graphOffsetY, graphPosition.y,
getSourceEndpointId(edge) getSourceEndpointId(edge)
), ),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -50,16 +50,16 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
) )
const [targetTop, setTargetTop] = useState( const [targetTop, setTargetTop] = useState(
getEndpointTopOffset(targetEndpoints, graphOffsetY, edge?.to.stepId) getEndpointTopOffset(targetEndpoints, graphPosition.y, edge?.to.stepId)
) )
useLayoutEffect(() => { useLayoutEffect(() => {
setTargetTop( setTargetTop(
getEndpointTopOffset(targetEndpoints, graphOffsetY, edge?.to.stepId) getEndpointTopOffset(targetEndpoints, graphPosition.y, edge?.to.stepId)
) )
}, [ }, [
targetBlockCoordinates?.y, targetBlockCoordinates?.y,
targetEndpoints, targetEndpoints,
graphOffsetY, graphPosition.y,
edge?.to.stepId, edge?.to.stepId,
]) ])

View File

@@ -1,16 +1,19 @@
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react' import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
import React, { useRef, useMemo, useEffect, useState } from 'react' import React, { useRef, useMemo, useEffect, useState } from 'react'
import { blockWidth, useGraph } from 'contexts/GraphContext' import {
import { BlockNode } from './Nodes/BlockNode/BlockNode' blockWidth,
graphPositionDefaultValue,
useGraph,
} from 'contexts/GraphContext'
import { useStepDnd } from 'contexts/GraphDndContext' import { useStepDnd } from 'contexts/GraphDndContext'
import { Edges } from './Edges'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader' import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
import { Block, DraggableStepType, PublicTypebot, Typebot } from 'models' import { DraggableStepType, PublicTypebot, Typebot } from 'models'
import { generate } from 'short-uuid' import { generate } from 'short-uuid'
import { AnswersCount } from 'services/analytics' import { AnswersCount } from 'services/analytics'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable' import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
import GraphContent from './GraphContent'
declare const window: { chrome: unknown | undefined } declare const window: { chrome: unknown | undefined }
@@ -30,19 +33,17 @@ export const Graph = ({
const editorContainerRef = useRef<HTMLDivElement | null>(null) const editorContainerRef = useRef<HTMLDivElement | null>(null)
const { createBlock } = useTypebot() const { createBlock } = useTypebot()
const { const {
graphPosition, setGraphPosition: setGlobalGraphPosition,
setGraphPosition,
setOpenedStepId, setOpenedStepId,
updateBlockCoordinates, updateBlockCoordinates,
setGraphOffsetY,
} = useGraph() } = useGraph()
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
const [debouncedGraphPosition] = useDebounce(graphPosition, 200) const [debouncedGraphPosition] = useDebounce(graphPosition, 200)
const transform = useMemo( const transform = useMemo(
() => () =>
`translate(${graphPosition.x}px, ${graphPosition.y}px) scale(${graphPosition.scale})`, `translate(${graphPosition.x}px, ${graphPosition.y}px) scale(${graphPosition.scale})`,
[graphPosition] [graphPosition]
) )
const [isMouseDown, setIsMouseDown] = useState(false)
useEffect(() => { useEffect(() => {
editorContainerRef.current = document.getElementById( editorContainerRef.current = document.getElementById(
@@ -53,10 +54,12 @@ export const Graph = ({
useEffect(() => { useEffect(() => {
if (!graphContainerRef.current) return if (!graphContainerRef.current) return
setGraphOffsetY( const { top, left } = graphContainerRef.current.getBoundingClientRect()
graphContainerRef.current.getBoundingClientRect().top + setGlobalGraphPosition({
debouncedGraphPosition.y x: left + debouncedGraphPosition.x,
) y: top + debouncedGraphPosition.y,
scale: 1,
})
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedGraphPosition]) }, [debouncedGraphPosition])
@@ -71,7 +74,6 @@ export const Graph = ({
}) })
} }
const handleGlobalMouseUp = () => setIsMouseDown(false)
const handleMouseUp = (e: MouseEvent) => { const handleMouseUp = (e: MouseEvent) => {
if (!typebot) return if (!typebot) return
if (!draggedStep && !draggedStepType) return if (!draggedStep && !draggedStepType) return
@@ -98,26 +100,14 @@ export const Graph = ({
const handleClick = () => setOpenedStepId(undefined) const handleClick = () => setOpenedStepId(undefined)
const handleMouseDown = () => setIsMouseDown(true)
const handleMouseMove = (event: React.MouseEvent) => {
if (!isMouseDown) return
const { movementX, movementY } = event
setGraphPosition({
x: graphPosition.x + movementX / (window.chrome ? 2 : 1),
y: graphPosition.y + movementY / (window.chrome ? 2 : 1),
scale: 1,
})
}
useEventListener('wheel', handleMouseWheel, graphContainerRef.current) useEventListener('wheel', handleMouseWheel, graphContainerRef.current)
useEventListener('mousedown', handleCaptureMouseDown, undefined, { useEventListener('mousedown', handleCaptureMouseDown, undefined, {
capture: true, capture: true,
}) })
useEventListener('mouseup', handleMouseUp, graphContainerRef.current) useEventListener('mouseup', handleMouseUp, graphContainerRef.current)
useEventListener('mouseup', handleGlobalMouseUp)
useEventListener('click', handleClick, editorContainerRef.current) useEventListener('click', handleClick, editorContainerRef.current)
const onDrag = (event: DraggableEvent, draggableData: DraggableData) => { const onDrag = (_: DraggableEvent, draggableData: DraggableData) => {
const { deltaX, deltaY } = draggableData const { deltaX, deltaY } = draggableData
setGraphPosition({ setGraphPosition({
x: graphPosition.x + deltaX, x: graphPosition.x + deltaX,
@@ -128,13 +118,7 @@ export const Graph = ({
return ( return (
<DraggableCore onDrag={onDrag}> <DraggableCore onDrag={onDrag}>
<Flex <Flex ref={graphContainerRef} position="relative" {...props}>
ref={graphContainerRef}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
position="relative"
{...props}
>
<Flex <Flex
flex="1" flex="1"
w="full" w="full"
@@ -146,14 +130,10 @@ export const Graph = ({
willChange="transform" willChange="transform"
transformOrigin="0px 0px 0px" transformOrigin="0px 0px 0px"
> >
<Edges <GraphContent
edges={typebot?.edges ?? []}
answersCounts={answersCounts} answersCounts={answersCounts}
onUnlockProPlanClick={onUnlockProPlanClick} onUnlockProPlanClick={onUnlockProPlanClick}
/> />
{typebot?.blocks.map((block, idx) => (
<BlockNode block={block as Block} blockIndex={idx} key={block.id} />
))}
</Flex> </Flex>
</Flex> </Flex>
</DraggableCore> </DraggableCore>

View File

@@ -0,0 +1,31 @@
import { useTypebot } from 'contexts/TypebotContext'
import { Block } from 'models'
import React from 'react'
import { AnswersCount } from 'services/analytics'
import { Edges } from './Edges'
import { BlockNode } from './Nodes/BlockNode'
type Props = {
answersCounts?: AnswersCount[]
onUnlockProPlanClick?: () => void
}
const MyComponent = ({ answersCounts, onUnlockProPlanClick }: Props) => {
const { typebot } = useTypebot()
return (
<>
<Edges
edges={typebot?.edges ?? []}
answersCounts={answersCounts}
onUnlockProPlanClick={onUnlockProPlanClick}
/>
{typebot?.blocks.map((block, idx) => (
<BlockNode block={block as Block} blockIndex={idx} key={block.id} />
))}
</>
)
}
// Performance hack, never rerender when graph (parent) is panned
const areEqual = () => true
export default React.memo(MyComponent, areEqual)

View File

@@ -73,8 +73,6 @@ const graphContext = createContext<{
openedStepId?: string openedStepId?: string
setOpenedStepId: Dispatch<SetStateAction<string | undefined>> setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
isReadOnly: boolean isReadOnly: boolean
graphOffsetY: number
setGraphOffsetY: Dispatch<SetStateAction<number>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({ }>({
@@ -100,8 +98,6 @@ export const GraphProvider = ({
const [blocksCoordinates, setBlocksCoordinates] = useState<BlocksCoordinates>( const [blocksCoordinates, setBlocksCoordinates] = useState<BlocksCoordinates>(
{} {}
) )
const [graphOffsetY, setGraphOffsetY] = useState(0)
useEffect(() => { useEffect(() => {
setBlocksCoordinates( setBlocksCoordinates(
blocks.reduce( blocks.reduce(
@@ -153,8 +149,6 @@ export const GraphProvider = ({
blocksCoordinates, blocksCoordinates,
updateBlockCoordinates, updateBlockCoordinates,
isReadOnly, isReadOnly,
graphOffsetY,
setGraphOffsetY,
}} }}
> >
{children} {children}