Reactive state
field / derived / state, the global store, read/watch.
flash-theater's core pitch is: declare data, bind it into the template, and never write an
observeFieldScoped or a manual setField call
yourself. Everything below compiles straight to SceneGraph onChange handlers.
field and derived
field declares a real SceneGraph interface field — public, externally-settable
data (a prop), typed as one of string | integer | float | boolean | node | array | assocarray,
with the default literal's own shape checked against the declared type at compile time.
derived declares a value recomputed automatically whenever anything it reads
changes — dependencies are inferred statically, not declared by hand, and a dependency cycle is a compile error.
Its own declared type is checked too: the compiler infers a best-effort static type for the expression (literals,
field/state/derived references, arithmetic, comparisons, a call to a same-script function or a
ClassName(...).method()) and rejects a confirmed mismatch — anything it can't
confidently infer (a builtin call, a theme.*/member access, ...) is never
flagged, so object/dynamic stays the escape
hatch for anything more dynamic than that.
<script>
field width: integer = 200
field height: integer = 60
derived isWide: boolean = width > 300
derived label: string = describeSize(width, height)
private function describeSize(w: integer, h: integer): string {
return str(w) + "x" + str(h)
}
</script>
<component>
<Rectangle id="box" width="{width}" height="{height}">
<Label id="caption" text="{label}" />
</Rectangle>
</component>state — private, writable, reactive
SceneGraph has no privacy mechanism for interface fields — every <field> is
externally reachable, so it can never be genuine component-local state. state is
a private m.x member instead — unreachable from outside the component — with
its own dedicated write statement, state <name> = <expr>, since a
private member has no SceneGraph field observer to auto-fire a change cascade the way a field
write does.
state showCelebration: boolean = false
state tierLabel: string = "starting out"
public function addFavorite() {
newCount = favoriteCount + 1
store(favoriteCount) = newCount
state tierLabel = newCount < 3 ? "starting out" : newCount < 10 ? "collector" : "super fan"
}
A plain assignment to a name that happens to match a field's name is
not a hidden field write — it's an ordinary new local, shadowing the field
for the rest of that function (real BrightScript scoping, deliberately not special-cased). There is currently no
syntax for mutating a field from inside the component's own code — use
state for anything that needs to be both private and writable.
The global store
A schemaless, zero-declaration global key/value store, built in — no <store>
tag to write (that was removed). Read a value with read (one-time snapshot) or
watch (reactive — recomputes whenever that top-level key changes), and write
with the store(<key>) = <expr> statement.
<script>
watch favoriteCount = store(favoriteCount)
derived favoritesLabel: string = describeFavorites(favoriteCount)
private function describeFavorites(count: integer): string {
if (count == 1) {
return "1 favorite"
}
return str(count) + " favorites"
}
public function addFavorite() {
store(favoriteCount) = favoriteCount + 1
}
</script>
<component>
<Label id="label" text="{favoritesLabel}" />
</component>read initialCount = store(favoriteCount) ' one-time snapshot, taken once
watch favoriteCount = store(favoriteCount) ' reactive — recomputes on every store write
A store write can only ever replace a whole top-level key, never a nested
path (store(some.value) = 2 is a compile error) — this traces back to real
SceneGraph semantics: a field observer only fires on reassignment of the field itself, never on an in-place
mutation of something already stored in it, so a nested-path write would either not work or silently fail to
notify any watch. theme.a.b access works the
same way — see the Theme page.
Array and assocarray defaults
field/state both accept
array/assocarray as a declared type, with a
literal default — the literal's contents must be pure literals (no identifiers, no calls); use
derived for anything computed.
field tags: array = ["news", "sports"]
field config: assocarray = { retries: 3, timeout: 10 }
state items: array = []Reference implementation — apps/reactive-state-demo
Every mechanism on this page has a router-mounted, scaled
chapter in apps/reactive-state-demo — 4 chapters
(/field-and-derived through
/array-and-assocarray-defaults), reachable with
REWIND/FAST-FORWARD once compiled and sideloaded. Each chapter shows a default,
no-customization example alongside a deliberately different, customized one — e.g.
GlobalStoreDemo.thr puts a read
and a watch on the SAME store key side by side, so pressing
"Bump store(demoCount)" visibly updates the watch-bound
label while the read-bound one stays frozen at its initial
value — the one-time-snapshot-vs-reactive distinction, live. This is the live, compiling
reference for the whole page — see findings/reactive-state-demo-app.md
for what each chapter covers and findings/demo-app-conventions.md
for the app-structure convention it follows.
⚠️ Not (yet) supported
- ○ Writing to a plain
fieldfrom inside the component's own code — there's no dedicated grammar for it; usestateinstead. - ○ A nested-path store write (
store(a.b) = x) — only a whole top-level key can be reassigned. - ○
state's declared type is decorative, not checked against its default literal's shape (unlikefield, which is checked) — only array/assocarray *contents* are validated forstate.
Exact grammar: GRAMMAR.md. Full feature status: docs/features.md.