The current version of the framework contains numerous breaking changes compared to its predecessor.
Let's, start by exploring the differences between v1 and v2, both from a developer's and user's point of view:
| Context | v1 | v2 | Reference | 
|---|---|---|---|
| TypeScript | Partial support | TypeScript-first | |
| Quick start | Codepen examples | DevKit, Codesandbox examples | |
| Architecture | Event-based | Middleware-like signals | |
| Tools | rete-cli | rete-cli,rete-kit,rete-qa | |
| Testing | unit testing | unit + E2E testing | |
| UI | |||
| Nodes order | fixed order | bring forward picked nodes | |
| Selection | built-in for nodes only | advanced selection + custom elements | |
| Controls | no built-in controls provided | built-in classic input control | |
| Arrange nodes | limited | powered by elkjs | |
| Code | |||
| Node creation | Component-based approach | up to you | |
| Editor/Engine identifiers | mandatory, required for import/export | up to you | |
| Node identifier | incremental decimal id | unique id | |
| Import/export | Built-in, limited | up to you | |
| Validation | Socket-based validation | up to you | |
| Dataflow processing | limited (no recursion) | DataflowEnginewith dynamic fetching | |
| Control flow processing | simulated by Task plugin with limitations | ControlFlowEngine | |
| Modules | rete-module-plugin | up to you | |
| Connection plugin | responsible for both rendering and interaction | responsible for interaction only | 
Connect the plugin by importing it by default import.
The second parameter is used for passing the plugin's options/parameters:
// v1
import HistoryPlugin from 'rete-history-plugin';
editor.use(HistoryPlugin, { keyboard: true });
All plugins are implemented as classes and can be extended, providing flexible customization without modifying the core.
// v2
import { HistoryPlugin, HistoryExtensions, Presets } from 'rete-history-plugin'
const history = new HistoryPlugin<Schemes>()
history.addPreset(Presets.classic.setup())
HistoryExtensions.keyboard(history)
area.use(history)
In the v1, nodes are generated via components that were registered within the editor, which enabled the creation of numerous instances of nodes belonging to the same Component type.
// v1
class NumComponent extends Rete.Component {
  constructor(){
    super("Number");
  }
  builder(node) {
    node.addControl(new NumControl('num'))
    node.addOutput(new Rete.Output('num', "Number", numSocket))
    return node
  }
}
const numComponent = new NumComponent()
editor.register(numComponent);
const node = await numComponent.createNode({ num: 2 });
The current version doesn't include Component as an abstraction, but you can implement similar approach if needed.
// v2
const node = new ClassicPreset.Node('Number')
node.addControl('num', new NumControl('num'))
node.addOutput('num', new ClassicPreset.Output(numSocket, "Number"));
await editor.addNode(node)
The data can be saved using method putData. It is expected that the data should be in a valid JSON format, as it may be used for import/export.
// v1
node.putData('myData', 'data')
control.putData('myData', 'data') // where control is part of node
There are no rigid import/export guidelines to follow in the current version, which means you have complete flexibility in how you store your data in nodes.
// v2
class MyNode extends ClassicPreset.Node {
  myData = 'data'
}
Because of the limitations mentioned earlier, the editor can be effortlessly exported and imported.
// v1
const data = editor.toJSON();
await editor.fromJSON(data);
The current version incorporates a revised approach that requires implementation, as demonstrated in Import/export.
Selecting elements is a feature integrated within the editor
// v1
editor.selected.list
editor.selected.add(node, accumulate)
The downside to this implementation is its incapability to support anything other than node selection.
The selection of nodes (and other elements) looks like:
// v2
const selector = AreaExtensions.selector()
const accumulating = AreaExtensions.accumulateOnCtrl()
const nodeSelector = AreaExtensions.selectableNodes(area, selector, { accumulating });
editor.getNodes().filter(node => node.selected)
nodeSelector.select(add.id)
The typical way to listen to events that can be prevented
// v1
editor.on('nodecreate', node => {
 return node.canCreate
});
* - unchanged
** - moved to different package
*** - removed
rete package eventsrete-connection-plugin package eventsThe current version uses a specific kind of signal implementation that involves object-based signals. Additionally, pipes are used to either manipulate these objects or prevent signal propagation.
// v2
editor.addPipe(context => {
  if (context.type === 'nodecreate') return
  return context
})
rete package eventsrete-area-plugin package eventsrete-connection-plugin package eventsrete-angular-plugin package eventsrete-vue-plugin package eventsrete-react-plugin package eventsThere is a built-in connection validation based on socket compatibility
// v1
const anyTypeSocket = new Rete.Socket('Any type');
numSocket.combineWith(anyTypeSocket);
This approach is simple but has some limitations.
Connection validation can be implemented independently, that provides more flexibility.
// v2
editor.addPipe(context => {
  if (context.type === 'connectioncreate') {
    if (canCreateConnection(context.data)) return false
  }
  return context
})
The component with defined worker method should be registered
// v1
const engine = new Rete.Engine('demo@0.1.0');
engine.register(myComponent);
Define worker method of the component
// v1
worker(node, inputs, outputs){
  outputs['num'] = node.data.num;
}
Trigger the processing
// v1
await engine.process(data);
Create the DataflowEngine instance to connect to the editor. Unlike the first version, there is no need to pass data with nodes and connections.
// v2
import { DataflowEngine } from 'rete-engine'
const engine = new DataflowEngine<Schemes>()
editor.use(engine)
Node method example
// v2
data(inputs) {
  const { left, right } = inputs
  return { sum: left[0] + right[0] }
}
Start the processing
// v2
engine.fetch(node.id)
This approach is implemented using the rete-task-plugin and based on the Rete.Engine. Therefore, it has the aforementioned limitations
// v1
import TaskPlugin from 'rete-task-plugin';
editor.use(TaskPlugin);
Component's constructor has specified outputs that are intended for control flow or dataflow
// v1
this.task = {
    outputs: { exec: 'option', data: 'output' },
    init(task) {
        task.run('any data');
        task.reset();
    }
}
Define the worker method, which returns data and specifies closed output ports for control flow
// v1
worker(node, inputs, data) {
    this.closed = ['exec'];
    return { data }
}
The rete-engine package is used, which has a separate implementation of the engine for control flow
// v2
import { ControlFlowEngine } from 'rete-engine'
const engine = new ControlFlowEngine<Schemes>()
editor.use(engine)
By default, all ports are configured to pass control, but you can designate certain ones for this
// v2
const engine = new ControlFlowEngine<Schemes>(() => {
  return {
    inputs: () => ["exec"],
    outputs: () => ["exec"]
  };
});
The following serves as the node method:
// v2
execute(input: 'exec', forward: (output: 'exec') => void) {
  forward('exec')
}
Unlike the previous version, this approach is completely decoupled from the dataflow. Nevertheless, it can be used in conjunction with DataflowEngine.
// v2
async execute(input: 'exec', forward: (output: 'exec') => void) {
  const inputs = await dataflow.fetchInputs(this.id)
  forward('exec')
}
As a demonstration, we have opted to use rete-react-render-plugin
// v1
import ReactRenderPlugin from 'rete-react-render-plugin';
editor.use(ReactRenderPlugin)
// v2
import { ReactPlugin } from 'rete-react-plugin'
const reactPlugin = new ReactPlugin<Schemes, AreaExtra>()
area.use(reactPlugin)
The following code is used to specify the components needed for specific nodes and controls
// v1
class AddComponent extends Rete.Component {
  constructor() {
    super("Add");
    this.data.component = MyNode;
  }
}
class MyControl extends Rete.Control {
  constructor(emitter, key, name) {
    super(key);
    this.render = 'react';
    this.component = MyReactControl;
    this.props = { emitter, name };
  }
}
Alternatively, component can be specified for all nodes
// v1
editor.use(ReactRenderPlugin, { component: MyNode });
In this version, the components to be visualized are defined in the classic preset that is connected
// v2
reactPlugin.addPreset(ReactPresets.classic.setup({ customize: {
  node(data) {
    return MyNode
  },
  control() {
    return MyReactControl
  }
}}))
This approach offers greater flexibility, enabling you to define additional conditions within the handlers
Retrieve the view of the node and execute its translate method
// v1
editor.view.nodes.get(node).translate(x, y)
The plugin instance contains translate method that only needs the node identifier.
// v2
await area.translate(node.id, { x, y })
The plugin offers approach for positioning nodes, but its functionality is significantly restricted.
// v1
import AutoArrangePlugin from 'rete-auto-arrange-plugin';
editor.use(AutoArrangePlugin, {});
editor.trigger('arrange');
The plugin leverages the advanced functionality of the elkjs package.
// v2
import { AutoArrangePlugin, Presets as ArrangePresets } from "rete-auto-arrange-plugin";
const arrange = new AutoArrangePlugin<Schemes>();
arrange.addPreset(ArrangePresets.classic.setup());
area.use(arrange);
await arrange.layout()
The zoomAt method requires an editor instance that is responsible for visualization
// v1
import AreaPlugin from "rete-area-plugin";
AreaPlugin.zoomAt(editor);
For visualization purposes in this version, an instance of AreaPlugin is required.
// v2
import { AreaExtensions } from "rete-area-plugin";
AreaExtensions.zoomAt(area, editor.getNodes());