#From Roact

Roact renders a virtual tree and reconciles it. Vision builds a real tree once and writes properties into it. That is the whole difference; everything below follows from it.

#The table

Roact Vision
Roact.createElement("Frame", props, children) { ClassName = "Frame", ...props, ...children }
a function component a function returning a declaration table
a class component a function returning a declaration table, plus event for its state
self.state / self:setState event("Name", Initial, Callback) and Interface.Name(Value)
Roact.Event.Activated = fn Activated = fn
Roact.Change.Text = fn PropertyChanged = { Text = fn }
Roact.Ref Interface:Open() or ready(function(self) end)
didMount ready(function(self, vision) end)
willUnmount cleanup(function(self, vision) end)
Roact.mount(element, parent, name) Scope:Capture(Declaration) then Interface:Mount()
Roact.unmount(handle) Interface:Cleanup()
Roact.createFragment the array part of a declaration
Roact.createBinding event plus merge
Roact.None not needed, nothing merges props

#A counter

Roact:

local Counter = Roact.Component:extend("Counter")

function Counter:init()
    self:setState({ Count = 0 })
end

function Counter:render()
    return Roact.createElement("TextButton", {
        Text = `Clicks: {self.state.Count}`,
        [Roact.Event.Activated] = function()
            self:setState({ Count = self.state.Count + 1 })
        end,
    })
end

local Handle = Roact.mount(Roact.createElement(Counter), PlayerGui)

Vision:

local Counter = Scope:Capture({
    ClassName = "TextButton",

    mount(PlayerGui),

    event("Count", 0, function(self, _, Value)
        self.Text = `Clicks: {Value}`
    end),

    Activated = function(_, Panel)
        Panel.Count(Panel.Count() + 1)
    end,
})

Counter:Mount()

There is no render. The callback bound to Count runs when Count changes and assigns one property. Nothing else in the tree is touched.

#Things that will trip you

Props do not flow down. A Roact component takes props and passes them to children. A Vision function takes plain Lua arguments and bakes them into the table it returns, once. If a child needs to react to something later, give the value a name and merge onto it from the child.

State does not live in a component. It lives on the vision. Any node in the same declaration can merge onto the same name and get its own callback, so one write can drive twenty instances without a re-render.

setState was batched, value writes are not. Interface.Count(1) runs its callbacks immediately. Writing the same value twice in a row is skipped, but two different writes fire twice.

Reconciliation is gone. Roact let you return a different tree and worked out the difference. In Vision the tree is fixed at Capture. For a list that changes, keep a handle per row and mount or clean them yourself.

Portals are just mount. Any node can carry mount(SomeInstance) and land somewhere other than its parent, including a node named elsewhere in the same declaration.