Events + actions

Events trigger actions. Actions update state or perform effects.

Events connect UI to behavior

Events describe when something happens (click, input, submit, mount). Actions describe what should happen in response (update state, call HTTP, navigate, delay, etc.).

In Thorm, events and actions are both declared inside the IR. That keeps behavior explicit and predictable: it is part of the UI tree, not scattered in hidden callbacks.

Attaching events

Use on(event, action) to attach an action to an event. The event name is a string (e.g., click, input, submit), and the action can be a single action or a composed task.

Because events are props, they live in the same place as attrs() and cls(). This makes it easy to read a node and understand both its structure and its behavior.

Common action helpers

Actions are built with helpers. Some update atoms directly, others trigger side effects.

  • set($atom, value) updates state to a new value
  • inc($atom, by) increments numeric state
  • add($atom, by) adds an expression or value to state
  • http(...) performs a request and optionally stores output in atoms
  • navigate(...) changes the client-side route

Action helpers can be used directly on events or wrapped inside task() when you need multiple steps.

Composing actions with task

When a single event needs multiple steps, use task([...]) to run actions in order. This keeps complex behavior linear and easy to read.

You can also insert delay(ms, [...]) between steps for user feedback or animations.

If you need the current event payload, use ev(...) to read values from the DOM event. This is common for inputs or custom handlers.

Example: multi-step actions with task()

This example shows how task() chains actions in order. The click handler updates status, logs a message, waits 300ms, increments the counter, then resets the status.

This pattern scales well when a single event needs multiple state changes or side effects. It also keeps behavior readable compared to nested callbacks.

PHP

<?php
$count 
state(0);
$status state('idle');
$log state([]);

$app el('div', [attrs(['class' => 'container p-3'])], [
    
el('h1', [], [ text('Task action (CSR)') ]),
    
el('p', [], [ text(concat('Status: 'read($status))) ]),
    
el('p', [], [ text(concat('Count: 'read($count))) ]),
    
el('button', [
        
cls('btn btn-primary me-2'),
        
on('click'task([
            
set($statusval('starting...'), true),
            
push($logconcat('click #'read($count)), true),
            
delay(300, [
                
set($statusval('incremented'), true),
            ]),
            
inc($count1true),
            
set($statusval('idle'), true),
        ]))
    ], [ 
text('Run task') ]),
    
el('ul', [cls('mt-3')], [
        
repeat(read($log), item(''), el('li', [], [ text(item('')) ]))
    ]),
    
onMount([
        
task([
            
set($statusval('mounted'), true),
            
push($logval('mounted'), true),
            
delay(200, [ set($statusval('idle'), true) ]),
        ])
    ])
]);

Practical guidance

  • Use task() when you need multiple actions in response to one event.
  • Keep event handlers small; move complex workflows into composed actions.
  • Treat actions as data. They are part of the IR and should be readable like the rest of the tree.

Events and actions close the loop: they take user input and turn it into state changes or side effects.