Manage app-wide state with reactive globals

Declare a global on the app root; any component — however deeply nested — reads and writes it by name, with no prop-drilling and no AppState.

This is the current pattern for app-wide shared state. The AppState component is deprecated; the Migrating from AppState section below shows the one-to-one equivalent.

One global, read and written anywhere

A global declared on the App root is visible everywhere in the app. SessionBar (a user-defined component) reads it; ThemeToggle writes it; the read updates automatically:

<App global.session="{ { user: 'Ada', theme: 'light' } }">
  <VStack gap="$space-3" padding="$space-3">
    <SessionBar />
    <ThemeToggle />
  </VStack>
</App>

<Component name="SessionBar">
  <HStack verticalAlignment="center" gap="$space-2">
    <Text variant="strong">{session.user}</Text>
    <Text variant="secondary">theme: {session.theme}</Text>
  </HStack>
</Component>

<Component name="ThemeToggle">
  <Button
    label="Toggle theme"
    onClick="session = { ...session, theme: session.theme === 'light' ? 'dark' : 'light' }" />
</Component>
One global, read and written anywhere
<App global.session="{ { user: 'Ada', theme: 'light' } }">
  <VStack gap="$space-3" padding="$space-3">
    <SessionBar />
    <ThemeToggle />
  </VStack>
</App>

<Component name="SessionBar">
  <HStack verticalAlignment="center" gap="$space-2">
    <Text variant="strong">{session.user}</Text>
    <Text variant="secondary">theme: {session.theme}</Text>
  </HStack>
</Component>

<Component name="ThemeToggle">
  <Button
    label="Toggle theme"
    onClick="session = { ...session, theme: session.theme === 'light' ? 'dark' : 'light' }" />
</Component>

Neither component receives session as a prop. ThemeToggle reassigns it, and every reader — here SessionBar, in a real app any number of them — re-renders. Reassign the whole object ({ ...session, theme }) rather than mutating a field, so the change is a new value the reactive engine can see.

Declare globals in one of two places only: the root element of Main.xmlui (with global. or a <global> tag), or as top-level declarations in a Globals.xs code-behind file. They cannot be declared inside a user-defined component file.

var. on an ancestor vs a global

Reach for a global only when the state is genuinely app-wide. If the state belongs to one view, put a var. on the closest common ancestor instead — it is scoped, resets when that subtree unmounts, and won't collide with anything elsewhere.

  • Subtree (var. on a common ancestor) — a filter applied on this page, a tab selected in this dialog. The right default. See Communicate between sibling components.
  • Global (global. on the app root) — current user, theme, feature flags: anything read from inside arbitrary user-defined components without prop-threading, that should persist across navigation.

The practical trigger to go global: at least one consumer is a user-defined component nested below the common ancestor. Subtree var.s don't cross user-defined-component boundaries; globals do. Scoping has the full mechanics and the reasons not to reach for globals by default (instances couple, names collide, state goes stale across navigation).

Migrating from AppState

AppState stored a bucket and mutated it through update(). A global holds the same object and is updated by plain assignment:

<!-- Legacy: AppState -->
<App>
  <AppState id="settings" bucket="settings" initialValue="{ { theme: 'light' } }" />
  <Button label="Dark" onClick="settings.update({ theme: 'dark' })" />
  <Text>{settings.value.theme}</Text>
</App>
<!-- Current: reactive global -->
<App global.settings="{ { theme: 'light' } }">
  <Button label="Dark" onClick="settings = { ...settings, theme: 'dark' }" />
  <Text>{settings.theme}</Text>
</App>

The mapping:

AppStatereactive global
<AppState bucket="settings" initialValue="{…}" />global.settings="{…}" on the App root
read settings.value.themeread settings.theme
write settings.update({ theme: 'dark' })settings = { ...settings, theme: 'dark' }
settings.appendToList('ids', id)settings = { ...settings, ids: [...settings.ids, id] }

Key points

Declare globals only on the app root or in Globals.xs. A global. on a nested element or inside a user-defined component file is an error.

Reassign, don't mutate. Write a new object ({ ...state, field: value }) so the reactive engine sees a new value. Mutating a field in place may not trigger readers.

Prefer the smallest scope that works. A var. on a common ancestor is the default; go global only when a nested user-defined component must read or write the state, or it must outlive the view.


See also