Skip to content

Browser Block ​

The browser block defines everything that runs in the browser. It compiles to a JavaScript module that is embedded into an HTML page along with the Tova reactive runtime. The browser block uses a fine-grained reactive system inspired by SolidJS, where signals, computed values, and effects automatically track dependencies and update the DOM with minimal overhead.

Purpose ​

The browser block is where your application's UI lives:

  • Reactive state -- signals that trigger updates when they change
  • Computed values -- derived data that recalculates automatically
  • Effects -- side effects that re-run when their dependencies change
  • Components -- reusable UI elements with JSX
  • Event handlers -- user interaction logic
  • Server calls -- invoking server functions via the RPC bridge

Reactive Primitives ​

State (Signals) ​

A state declaration creates a reactive signal. When its value changes, everything that depends on it updates automatically:

tova
browser {
  state count = 0
  state name = "World"
  state users: [User] = []
  state loading = false
}

Reading a signal returns its current value. Assigning to it updates it and triggers reactivity:

tova
browser {
  state count = 0

  fn increment() {
    count = count + 1      // Triggers all dependent updates
  }

  fn reset() {
    count = 0
  }
}

Under the hood, state count = 0 compiles to:

javascript
const [count, setCount] = createSignal(0);

Reading count in generated code calls count() (the getter). Writing count = 5 calls setCount(5) (the setter). This is all handled by the compiler transparently.

Computed Values ​

Computed values derive from other reactive values and update automatically when dependencies change:

tova
browser {
  state price = 10
  state quantity = 2

  computed total = price * quantity
  computed display = "Total: {total}"
}

A computed value is read-only. You cannot assign to it -- it always reflects the latest derived value.

Under the hood, computed total = price * quantity compiles to:

javascript
const total = createComputed(() => price() * quantity());

Effects ​

Effects are side-effect functions that re-execute whenever their reactive dependencies change:

tova
browser {
  state users: [User] = []

  effect {
    users = server.get_users()
  }
}

This effect runs once on initialization, calling the server to load users. If any signal it reads changes, it re-runs.

Effects are the primary way to:

  • Load data from the server
  • Update the document title
  • Log state changes
  • Synchronize with external systems
tova
browser {
  state search_query = ""
  state results: [User] = []

  effect {
    if search_query.length() > 2 {
      results = server.search_users(search_query)
    }
  }
}

When search_query changes (and is longer than 2 characters), the effect automatically re-runs and fetches new results.

Components ​

Components are reusable UI elements defined with JSX:

tova
browser {
  component App {
    <div>
      <h1>Hello, Tova!</h1>
      <p>Count: {count}</p>
      <button on:click={increment}>Increment</button>
    </div>
  }
}

Component Props ​

Components can accept props:

tova
browser {
  component UserCard(user) {
    <div class="card">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  }

  component App {
    <div>
      for user in users {
        <UserCard user={user} />
      }
    </div>
  }
}

Event Handling ​

Use on:event syntax to bind event handlers:

tova
browser {
  component App {
    <div>
      <button on:click={increment}>+1</button>
      <input on:input={fn(e) { name = e.target.value }} />
      <form on:submit={handle_submit}>
        // ...
      </form>
    </div>
  }
}

Conditional Rendering ​

Use if/else inside JSX:

tova
browser {
  component App {
    <div>
      if loading {
        <p>Loading...</p>
      } else {
        <ul>
          for user in users {
            <li>{user.name}</li>
          }
        </ul>
      }
    </div>
  }
}

List Rendering ​

Use for to iterate:

tova
browser {
  component TodoList {
    <ul>
      for todo in todos {
        <li class={if todo.done { "completed" } else { "" }}>
          {todo.text}
        </li>
      }
    </ul>
  }
}

Calling Server Functions ​

The browser block communicates with the server by calling functions through the server object. Every function defined in a server block is available as server.function_name():

tova
server {
  fn get_users() -> [User] {
    UserModel.all()
  }

  fn create_user(name: String, email: String) -> User {
    UserModel.create({ name, email })
  }

  fn delete_user(id: Int) -> Bool {
    UserModel.delete(id)
  }
}

browser {
  state users: [User] = []

  // Load on initialization
  effect {
    users = server.get_users()
  }

  // Call with arguments
  fn handle_create() {
    new_user = server.create_user(name, email)
    users = [...users, new_user]
  }

  // Call and update state
  fn handle_delete(id: Int) {
    server.delete_user(id)
    users = users.filter(fn(u) { u.id != id })
  }
}

All server.fn_name() calls are compiled to async RPC calls. Inside effects and event handlers, the compiler automatically adds await. See RPC Bridge for details on how this works.

Stores ​

For complex nested state, use stores:

tova
browser {
  store app_state = {
    user: { name: "", email: "" }
    settings: { theme: "light", notifications: true }
  }
}

Stores provide fine-grained reactivity for nested objects without requiring immutable update patterns.

Lifecycle Hooks ​

Components have lifecycle hooks:

tova
browser {
  component Dashboard {
    onMount {
      print("Dashboard mounted")
      data = server.load_dashboard()
    }

    onUnmount {
      print("Dashboard unmounted")
    }

    <div>
      // ...
    </div>
  }
}

CSS Injection ​

Components can include scoped styles:

tova
browser {
  component StyledButton(label) {
    style {
      .btn {
        background: blue;
        color: white;
        padding: 8px 16px;
        border-radius: 4px;
      }
    }

    <button class="btn">{label}</button>
  }
}

Multiple Browser Blocks ​

Multiple Blocks in One File ​

A single .tova file can contain multiple browser {} blocks. They are merged into one client output, sharing the same runtime scope. This lets you organize code by concern within a file:

tova
// State and data loading
browser {
  state users: [User] = []
  state loading = false

  effect {
    loading = true
    users = server.get_users()
    loading = false
  }
}

// Components
browser {
  component UserCard(user) {
    <div class="card">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  }

  component App {
    <div>
      if loading {
        <p>"Loading..."</p>
      } else {
        for user in users {
          <UserCard user={user} />
        }
      }
    </div>
  }
}

Both blocks merge — App can reference users, loading, and UserCard because they compile into the same module. State, computed values, effects, components, stores, and functions from all browser blocks in a file are combined.

Cross-File Browser Blocks (Same Directory) ​

All .tova files in the same directory are automatically merged by type. Browser blocks from different files share the same scope with no imports needed:

my-app/src/
  types.tova           # shared { type User { ... } }
  server.tova          # server { fn get_users() -> [User] { ... } }
  components.tova      # browser { component UserCard(user) { ... } }
  app.tova             # browser { state users = []; component App { ... } }

App in app.tova can use UserCard from components.tova directly — no import needed. The compiler merges all browser blocks from the directory into a single src.client.js output.

This is the recommended way to scale a Tova application: split by concern across files, and let the compiler handle the wiring.

Duplicate Detection ​

If two browser blocks (in the same file or across files in the same directory) declare the same top-level name, the compiler reports an error:

Error: Duplicate component 'App'
  → first defined in app.tova:15
  → also defined in main.tova:42

The following are checked for conflicts at the top level:

  • Component names
  • State variable names
  • Computed value names
  • Store names
  • Function names

Declarations scoped inside a component or store do not conflict. Two components can each have their own state count without issues:

tova
// components.tova — no conflict, count is scoped inside each component
browser {
  component Counter {
    state count = 0
    <button on:click={fn() count = count + 1}>{count}</button>
  }

  component Timer {
    state count = 0
    // ...
  }
}

Cross-Directory Browser Blocks ​

Files in different directories are compiled separately and do not auto-merge. Use explicit imports:

tova
// src/app.tova — import from subdirectory
import { SharedWidget } from "./widgets/shared.tova"

Each subdirectory produces its own output files. Cross-directory imports are rewritten to point to the generated .js files.

Named Browser Blocks ​

For applications that need entirely separate client outputs (e.g., an admin panel and a public site), use named browser blocks:

tova
browser "admin" {
  state adminUsers = []
  component AdminPanel {
    <div>"Admin Dashboard"</div>
  }
}

browser "public" {
  state posts = []
  component Blog {
    <div>"Public Blog"</div>
  }
}

Named blocks compile to separate files (app.client.admin.js, app.client.public.js) rather than merging. They do not share state or components. See Named Blocks for details.

When to use what

  • Multiple unnamed browser {} blocks (same file or same directory): Merged into one output. Use for organizing code by concern.
  • Named browser "name" {} blocks: Separate outputs. Use when you need completely independent client applications.

A Complete Example ​

Here is a complete browser block for a simple todo application:

tova
shared {
  type Todo {
    id: Int
    text: String
    done: Bool
  }
}

server {
  db { path: "./todos.db" }
  model Todo

  fn get_todos() -> [Todo] { TodoModel.all() }
  fn add_todo(text: String) -> Todo { TodoModel.create({ text, done: false }) }
  fn toggle_todo(id: Int) -> Todo { TodoModel.update(id, { done: not TodoModel.find(id).done }) }
  fn remove_todo(id: Int) -> Bool { TodoModel.delete(id) }
}

browser {
  state todos: [Todo] = []
  state new_text = ""

  computed remaining = len(todos.filter(fn(t) { not t.done }))

  effect {
    todos = server.get_todos()
  }

  fn handle_add() {
    guard new_text.length() > 0 else { return }
    todo = server.add_todo(new_text)
    todos = [...todos, todo]
    new_text = ""
  }

  fn handle_toggle(id: Int) {
    updated = server.toggle_todo(id)
    todos = todos.map(fn(t) { if t.id == id { updated } else { t } })
  }

  fn handle_remove(id: Int) {
    server.remove_todo(id)
    todos = todos.filter(fn(t) { t.id != id })
  }

  component App {
    <div>
      <h1>Todos ({remaining} remaining)</h1>

      <form on:submit={handle_add}>
        <input value={new_text} on:input={fn(e) { new_text = e.target.value }} />
        <button type="submit">Add</button>
      </form>

      <ul>
        for todo in todos {
          <li>
            <input type="checkbox" checked={todo.done} on:change={fn() { handle_toggle(todo.id) }} />
            <span class={if todo.done { "done" } else { "" }}>{todo.text}</span>
            <button on:click={fn() { handle_remove(todo.id) }}>x</button>
          </li>
        }
      </ul>
    </div>
  }
}

For detailed reference on each reactive primitive, see the Reactive UI pages:

Released under the MIT License.