State + expressions

Atoms hold state; expressions describe how that state becomes UI.

What state is in Thorm

State in Thorm lives in atoms. An atom holds a value and a stable identity that the runtime can observe. It is not a DOM node and it is not just a PHP variable. It is a piece of reactive data that the UI can depend on.

Once you read an atom inside a node, the runtime knows that node depends on that atom. When the atom changes, the node updates. This is the foundation of reactivity in Thorm.

Reading atoms creates expressions

Atoms become reactive only when you read them with read($atom). This turns the atom into an expression that can live in the IR tree.

Expressions are evaluated by the runtime, not by PHP. That is why the UI stays in sync: the runtime can track dependencies and update only the nodes that read a given atom.

Expressions are the glue

Expressions are composable. They let you build UI output from state without storing duplicated values. A small set of helpers covers most cases:

  • concat builds strings from literals and expressions
  • eq and cmp compare values for conditional rendering
  • cond chooses between two expressions
  • get reads nested values from arrays or objects
  • val wraps literals to make intent explicit

The key idea is to derive UI output instead of storing it. If a value can be computed from atoms, compute it in the view so it is always correct.

When to store state

Store only the source of truth. Everything else should be derived. This keeps state minimal and avoids bugs where derived values drift out of sync.

If you ever find yourself storing a value that can be calculated from other atoms, consider replacing it with an expression instead.

Example: reactive text

This is the smallest full loop. One atom stores the count. read() turns it into an expression. concat() builds a string. A click event runs inc(), and the text updates immediately.

This pattern scales: the same expression pipeline can drive attributes, classes, conditions, and list rendering.

PHP

<?php
$count 
state(1);

$app el('div', [ cls('container') ], [
    
el('h1', [], [ text('Reactive text')]),
    
el('p', [], [ text(concat('Count: 'read($count))) ]),
    
el('button',[
        
on('click'inc($count1)),
        
cls('btn btn-primary')
    ],[ 
        
text('Inc'
    ])
]);

Example: expressions with real data

This example shows expressions in a realistic flow. An input is bound to $amount, a form submit triggers http(), and the response is read with get().

Notice the output line: cond() branches on get(read($out), 'ok') and prints either a success message or a nested error message. That logic is declarative and always reflects the current response.

PHP

<?php
$app 
el('div', [
    
cls('container'),
], [
    
el('h1',[], [ text('Bid example') ]),
    
el('form', [
        
cls('my-5'),
        
on('submit'
            
http(
                
val('/api/bid/'),
                
'POST',
                
$out,
                
$status,
                [
'Content-Type' => 'application/x-www-form-urlencoded'],
                
concat('amount='read($amount)),
                
'json'
            
)
        )
    ], [
        
el('div', [ cls('col col-lg-2')], [
            
el('input'
                [ 
                    
cls('form-control shadow col-3'), 
                    
attrs(['type'=>'number']), ...bind($amount, ['type'=>'number']) 
                ]
            ),
        ] ),
        
el('button', [ cls('btn btn-primary mt-3') ], [ text('Place bid') ]),
        
el('p', [], [ text(get(read($out), 'message')) ]),

        
el('p', [], [
            
text(
                
cond(
                    
get(read($out), 'ok'),
                    
'Thanks!',
                    
get(get(read($out), 'error'), 'message')
                )
            )
        ])
    ])
]);

Practical guidance

  • Read atoms where you need them instead of caching derived values in state.
  • Keep expressions small and composable. If a single expression becomes too long, split it into smaller pieces.
  • Use get() for nested data instead of manual PHP access, so the runtime can track dependencies.

State and expressions are the core loop. Once you understand how they work together, the rest of the framework feels natural.