Last active
March 12, 2025 10:31
-
-
Save epicbytes/b28761eb9800ba9f53622ef2b7690ce8 to your computer and use it in GitHub Desktop.
Example how to use reactflow and effector together
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { NodeTypes } from "@reactflow/core/dist/esm/types"; | |
import { Edge, Node } from "reactflow"; | |
export type FlowEditorProps = { | |
nodeTypes?: NodeTypes; | |
nodes?: Node[]; | |
edges?: Edge[]; | |
library: FlowEditorLibrary | |
children?: JSX.Element; | |
}; | |
export type FlowEditorLibrarySection = { | |
name: string | |
items: FlowEditorLibraryElement[] | |
} | |
export type FlowEditorLibraryElement = any | |
export type FlowEditorLibrary = FlowEditorLibrarySection[] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { ReactElement, useCallback, useRef } from "react"; | |
import type { FlowEditorProps } from "./flow-editor.d"; | |
import { flowEditorModel } from "@/components/complex/flow-editor/model"; | |
import { modelView } from "effector-factorio"; | |
import { ReactFlow, NodeAddChange } from "reactflow"; | |
import "reactflow/dist/style.css"; | |
import { useGate, useStore } from "effector-react"; | |
export const FlowEditor = <T extends {}>( | |
props: FlowEditorProps | |
): ReactElement => { | |
const model = flowEditorModel.createModel(props); | |
useGate(model.FlowEditorGate); | |
return <FlowEditorElement model={model} />; | |
}; | |
export const FlowEditorElement = modelView(flowEditorModel, () => { | |
const reactFlowWrapper = useRef(null); | |
const { | |
$instance, | |
$nodes, | |
$edges, | |
onConnect, | |
onInit, | |
onNodesChange, | |
onEdgesChange, | |
onConnectStart, | |
nodeTypes, | |
library, | |
} = flowEditorModel.useModel(); | |
const nodes = useStore($nodes); | |
const edges = useStore($edges); | |
const reactFlowInstance = useStore($instance); | |
const onDragStart = (event, nodeType, item) => { | |
event.dataTransfer.setData("application/reactflow", nodeType); | |
event.dataTransfer.effectAllowed = "move"; | |
event.dataTransfer.setData("item", JSON.stringify(item)); | |
}; | |
const onDrop = useCallback( | |
(event) => { | |
event.preventDefault(); | |
// @ts-ignore | |
const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect(); | |
const type = event.dataTransfer.getData("application/reactflow"); | |
const item = JSON.parse(event.dataTransfer.getData("item")); | |
if (typeof type === "undefined" || !type) { | |
return; | |
} | |
const position = reactFlowInstance?.project({ | |
x: event.clientX - reactFlowBounds.left, | |
y: event.clientY - reactFlowBounds.top, | |
}); | |
const newNode = { | |
id: item.name, | |
type, | |
position, | |
data: item, | |
}; | |
onNodesChange([{ item: newNode, type: "add" } as NodeAddChange]); | |
}, | |
[reactFlowInstance] | |
); | |
const onDragOver = useCallback((event) => { | |
event.preventDefault(); | |
event.dataTransfer.dropEffect = "move"; | |
}, []); | |
return ( | |
<div className={"grid h-[500px] w-full grid-cols-[200px,1fr]"}> | |
<div> | |
{library.map((section, index) => ( | |
<ul className={"rounded-box bg-base-100 p-2"}> | |
<li key={index}> | |
<span>{section.name}</span> | |
<ul className={"rounded-box bg-base-100 p-2"}> | |
{section.items.map((item, index2) => ( | |
<li | |
key={index2} | |
draggable | |
className={"my-1"} | |
onDragStart={(event) => | |
onDragStart(event, "activity", item) | |
} | |
> | |
<span>{item.name}</span> | |
</li> | |
))} | |
</ul> | |
</li> | |
</ul> | |
))} | |
</div> | |
<div className={"h-full"} ref={reactFlowWrapper}> | |
<ReactFlow | |
nodes={nodes} | |
edges={edges} | |
onNodesChange={onNodesChange} | |
onEdgesChange={onEdgesChange} | |
nodeTypes={nodeTypes} | |
onInit={onInit} | |
onDrop={onDrop} | |
onDragOver={onDragOver} | |
onConnect={onConnect} | |
onConnectStart={(_, payload) => onConnectStart(payload)} | |
onConnectEnd={() => onConnectStart(null)} | |
fitView | |
attributionPosition="top-right" | |
/> | |
</div> | |
</div> | |
); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { modelFactory } from "effector-factorio"; | |
import { combine, createEvent, createStore } from "effector"; | |
import type { | |
Edge, | |
EdgeChange, | |
Node, | |
NodeDimensionChange, | |
NodePositionChange, | |
NodeSelectionChange, | |
Connection, | |
NodeChange, | |
NodeRemoveChange, | |
ReactFlowInstance, | |
EdgeRemoveChange, | |
EdgeSelectionChange, | |
EdgeAddChange, | |
OnConnectStartParams, | |
} from "reactflow"; | |
import { createGate } from "effector-react"; | |
const flowEditorModel = modelFactory((props) => { | |
const FlowEditorGate = createGate(); | |
const onNodesChange = createEvent<NodeChange[]>(); | |
const onEdgesChange = createEvent<EdgeChange[]>(); | |
const onConnect = createEvent<Connection>(); | |
const onInit = createEvent<ReactFlowInstance>(); | |
const onConnectStart = createEvent<OnConnectStartParams | null>(); | |
const $activeHandleSource = createStore<OnConnectStartParams | null>(null).on( | |
onConnectStart, | |
(state, payload) => payload | |
); | |
const $instance = createStore<ReactFlowInstance | null>(null).on( | |
onInit, | |
(state, payload) => payload | |
); | |
const $nodes = createStore<Node[]>(props.nodes || []).on( | |
onNodesChange, | |
(state, payload) => { | |
for (let item of payload) { | |
switch (item.type) { | |
case "remove": | |
state = state.filter( | |
(stateItem) => stateItem.id != (item as NodeRemoveChange).id | |
); | |
break; | |
case "dimensions": | |
state = state.map((stateItem) => | |
stateItem.id == (item as NodeDimensionChange).id | |
? { | |
...stateItem, | |
dimensions: (item as NodeDimensionChange).dimensions, | |
} | |
: stateItem | |
); | |
break; | |
case "position": | |
state = state.map((stateItem) => | |
stateItem.id == (item as NodePositionChange).id && | |
(item as NodePositionChange).dragging | |
? ({ | |
...stateItem, | |
position: (item as NodePositionChange).position, | |
positionAbsolute: (item as NodePositionChange) | |
.positionAbsolute, | |
dragging: (item as NodePositionChange).dragging, | |
} as Node) | |
: stateItem | |
); | |
break; | |
case "select": | |
state = state.map((stateItem) => | |
stateItem.id == (item as NodeSelectionChange).id | |
? { | |
...stateItem, | |
selected: (item as NodeSelectionChange).selected, | |
} | |
: { ...stateItem, selected: false } | |
); | |
break; | |
case "add": | |
state = [...state, item.item]; | |
break; | |
case "reset": | |
break; | |
} | |
return state; | |
} | |
} | |
); | |
const $edges = createStore<Edge[]>(props.edges || []) | |
.on(onEdgesChange, (state, payload) => { | |
for (let item of payload) { | |
switch (item.type) { | |
case "add": | |
const itm = (item as EdgeAddChange).item; | |
state = state.some( | |
(edge) => edge.target === itm.target && edge.source === itm.source | |
) | |
? state | |
: [...state, item.item]; | |
break; | |
case "remove": | |
state = state.filter( | |
(stateItem) => stateItem.id != (item as EdgeRemoveChange).id | |
); | |
break; | |
case "reset": | |
break; | |
case "select": | |
state = state.map((stateItem) => | |
stateItem.id == (item as EdgeSelectionChange).id | |
? { | |
...stateItem, | |
selected: (item as EdgeSelectionChange).selected, | |
} | |
: { ...stateItem, selected: false } | |
); | |
break; | |
} | |
} | |
return state; | |
}) | |
.on(onConnect, (state, payload) => { | |
return [ | |
...state, | |
{ | |
animated: true, | |
id: `edge-${payload.source}_${payload.target}`, | |
...payload, | |
}, | |
] as Edge[]; | |
}); | |
const mappingType = { | |
target: "input_fields", | |
source: "output_fields", | |
}; | |
const $activeConnector = combine( | |
$nodes, | |
$activeHandleSource, | |
(nodes, handle) => { | |
if (!handle) return null; | |
const node = nodes.find((item) => item.id === handle.nodeId); | |
if (!node) return null; | |
return node.data[mappingType[handle.handleType as string]].find( | |
(item) => item.name === handle.handleId | |
); | |
} | |
); | |
return { | |
FlowEditorGate, | |
onNodesChange, | |
onEdgesChange, | |
onInit, | |
onConnect, | |
onConnectStart, | |
$activeHandleSource, | |
$instance, | |
$nodes, | |
$edges, | |
$activeConnector, | |
nodeTypes: props.nodeTypes, | |
library: props.library, | |
}; | |
}); | |
export { flowEditorModel }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment