React was interesting; the state model mattered more
React’s 2013 release made one-way rendering tangible, but T04P improved only after its transport, continuity, and freshness states were made explicit outside the view library.
When React was released publicly in 2013, the part that caught my attention was not JSX or the promise of faster DOM updates. It was the blunt idea that a view could be described from state instead of maintained through a trail of mutations.
T04P had become a collection of imperative reactions. A socket callback changed a number, another toggled a class, a timer rewrote an age label, and reconnect code hid one banner while showing another. The DOM was both the output and an unofficial store of what the application believed.
I rebuilt one feed panel as a React experiment. The rendering became easier to reason about. The data did not become correct by association.
The useful lesson was separating an explicit product state from the mechanism that displayed it.
The old panel had state in four places
The entry panel looked simple: one count, one freshness label, a short history, and a connection explanation. Its behavior was spread across several locations.
The service state lived in JavaScript objects. The latest rendered count lived in a text node. CSS classes implied current or stale. A data-revision attribute helped reconnect logic remember what the panel had applied. The timer inferred age from the text currently visible.
No single value could answer, “What does this panel believe right now?”
An event handler might update the count and forget the revision attribute. The freshness timer could add .stale while a reconnect callback removed the page warning. Reading the DOM back into logic made presentation details part of the protocol.
Before trying React, I wrote one plain state shape:
type FeedViewState = {
projection: EntryProjection | null
lastContiguousRevision: number | null
observation: ObservationEvidence | null
transport: TransportState
continuity: ContinuityState
now: number
}The rendered panel would become a consequence of that object. Socket callbacks, timers, and recovery responses could propose state changes; they would no longer edit presentation independently.
That refactoring mattered even before a component rendered anything.
Rendering from state removed invalid visual combinations
The imperative version could accidentally show a green “live” label beside a reconnect warning because separate callbacks owned them. A render function could choose one presentation from the full combination.
I defined derived status separately:
if no projection -> awaiting first observation
if transport is closed -> last known; browser disconnected
if continuity is recovering -> last known; checking missed updates
if observation exceeds policy -> stale or unavailable
otherwise -> currentThe order was a product decision. A fresh observation at the service did not help a browser whose continuity was unknown. An open transport did not make an old source observation current.
React made it natural to express the resulting branches as one tree. The count, qualifier, and recovery action came from the same derived status. There was no code path that remembered to color the value and forgot to change its label.
The gain was not that conditional rendering was novel. I could generate markup from an object without React. The library made that discipline the comfortable default and handled bringing the existing DOM toward the new description.
Invalid visual combinations became harder to create because the view no longer accumulated independent mutations.
The component did not own the socket
My first experiment opened the Socket.IO connection inside the panel component. That made the demo self-contained and confused lifecycle, transport, and view ownership.
If the panel was removed and recreated, it could create another connection. If several panels needed the same feed, each might subscribe separately. Reconnect state would disappear with presentation state. Testing the view required a live transport.
I moved the socket and feed normalization into a session controller outside React. The controller produced explicit events and a current projection. The component received state and emitted narrow user intents such as retry or open diagnostics.
source event -> adapter -> session controller -> state transition -> component props
user action <- intent handler <------------------------------ viewThis boundary kept React from becoming the architecture. It was responsible for presentation and local interaction. The session controller remained responsible for continuity, revisions, recovery attempts, and source evidence.
The component could be rendered with an invented state object. That made its hard cases easy to inspect: old value during reconnect, missing first observation, conflicting human note, or current count with delayed equipment feed.
The socket could also be tested without a browser view. That independence became more valuable than the first successful render.
One-way data flow was a review tool
T04P already had events flowing from service to browser. Calling the architecture one-way would have hidden the callbacks that reached sideways into DOM state and the controls that mutated shared objects directly.
The experiment imposed a clearer sequence:
event or intent
-> transition
-> next state
-> derived presentationDuring review, I could ask which transition accepted an event, what previous state it required, and which next state it produced. If code changed the view without passing through that sequence, it stood out.
This did not make every update synchronous or simple. Recovery responses could arrive late. Timers caused freshness transitions. The browser could receive an event while configuration changed. Those cases still needed identities and ownership rules.
One-way rendering made the effects of those rules visible. It did not define the rules itself.
I found the distinction important because framework enthusiasm can turn a convenient direction of rendering into a claim that application data is solved. The data flow was only as trustworthy as the transitions feeding it.
Early examples made component state inviting. I initially copied the feed projection into the component and updated it when props changed. That created two owners and an awkward question after reconnect: was the controller's snapshot authoritative, or was the component's locally accumulated count?
The normalized feed state remained external and entered as props. Local state was reserved for presentation details that did not need protocol meaning: whether diagnostics were expanded, which history row had focus, or whether a small disclosure was open.
Even those choices required care. If an expanded row referred to an event evicted from bounded history, local state had to reconcile with the new props. React would not decide that policy automatically.
I wrote an ownership sentence for each value:
- The session controller owns feed projection and continuity.
- The freshness policy derives status from evidence and time.
- The component owns temporary disclosure state.
- The URL or configuration controller owns the selected feed.
The list prevented everything visible from being poured into component state merely because a setter existed.
React helped render state. It did not remove the need to decide where state belonged.
Time still needed an explicit input
A purely state-derived view seemed to imply that the UI changed only when data changed. Freshness changes while no event arrives. A current observation becomes delayed because time passes.
The first React panel called the clock inside its render method. That made the output vary for the same props and made tests race around threshold boundaries.
I treated now as an input supplied by a small clock controller. It updated at a cadence appropriate to the visible labels and recalculated immediately when the tab regained visibility. The freshness classifier remained a pure function of evidence, policy, and time.
The component did not increment an age counter. It rendered an age derived from the observation timestamp. A skipped timer tick could delay a repaint but could not make the observation permanently younger.
This made static scenarios possible:
observation: 18:40
now: 18:42
policy stale after: 90 seconds
expected panel: last known, stale explanationThe library rerendered when now changed. The product model decided what that change meant.
Time was another example of a hard domain input that component syntax could make easy to overlook.
The reducer came before I called it a reducer
I consolidated feed transitions into a plain function that accepted the previous state and an event. I did not have a grand state-management library or even consistent terminology for it yet.
function transition(state: FeedState, event: FeedEvent): FeedState {
switch (event.kind) {
case 'source-observation':
return applyObservation(state, event)
case 'transport-closed':
return markTransportClosed(state, event)
case 'recovery-started':
return beginRecovery(state, event)
case 'snapshot-received':
return applySnapshot(state, event)
}
}The real code also checked revision and attempt identity. An event that did not belong to the active stream could leave state unchanged and add a diagnostic record.
Keeping transition logic outside the component made tests about data behavior independent of markup. One fixture could verify that revision 12 arriving after 10 entered recovery. Another could render the resulting recovery state and inspect its explanation.
This split prevented a component test from becoming the only place where continuity was exercised. It also allowed the original non-React view to consume the same state while the experiment remained partial.
The name “reducer” became familiar later. The useful habit began as a response to too many callbacks editing too many places.
T04P did not become a React application overnight. I mounted the experimental feed panel inside the existing page and passed it normalized state. The history and notes panels remained imperative at first.
This boundary exposed a practical question: who owned the DOM region? React received one empty mount element and exclusive control inside it. Existing scripts stopped querying or decorating descendants of that element. Shared events went through the session controller rather than DOM events that both systems might handle.
The narrow island reduced migration risk. I could compare the two implementations, test long-running behavior, and remove the experiment without rewriting the transport. It also prevented a framework trial from becoming an excuse to postpone reliability work until a full conversion.
The cost was temporary duplication in presentation code and another script in the page budget. I measured that cost honestly. The experiment was useful in development; shipping React for one small panel was not automatically justified on the reference phone.
Incremental adoption meant the architecture had to be useful without the framework. That pressure improved the state boundary.
JSX was not the breakthrough
JSX was the most visibly unusual part of React and initially the easiest part to debate. Some markup expressions were clearer near their state branches; some teammates of the imaginary future in my head would surely dislike mixing them with JavaScript. None of that determined whether the dashboard understood freshness.
I tried the supported transformation setup in the prototype and kept the component small. The view tree was readable because the data states were named, not because angle brackets appeared in a JavaScript file.
An opaque expression such as connected && !loading ? ... remained opaque inside JSX. A derived feedPresentation.kind === 'recovering' carried the product concept. Better syntax could not rescue weak vocabulary.
Similarly, reusable components were helpful only at meaningful boundaries. I avoided building a generic StatusBadge that accepted arbitrary color and text. A FreshnessExplanation could encode consistent structure for source name, age, and limitation, but it still received domain state rather than a styling code.
React reduced the friction of describing UI from state. The quality of that description depended on the model beneath it.
Reconciliation did not prove source continuity
React's ability to update the DOM efficiently was often discussed with words such as reconciliation. T04P already used the same word for recovering missed feed events, and the collision was instructive.
DOM reconciliation answered how to make the rendered tree match the next view description. Feed reconciliation answered how to establish that the next data projection was complete after an interruption.
The first could succeed perfectly while the second failed. React could render revision 418 efficiently even if the browser had missed revisions 412 through 417.
I kept the terms qualified in notes and code: view reconciliation versus feed recovery. The component received a continuity state and rendered it; it did not infer continuity from a successful render.
This protected against a subtle form of framework confidence. A deterministic UI is only deterministic with respect to its inputs. If the input state is incomplete or stale, the framework can produce a beautifully consistent lie.
The dashboard incident had already taught that transport health was narrower than product truth. The React experiment added another boundary: rendering correctness was narrower too.
Moving every relevant fact into one object made the number of possible combinations visible. Transport had several states, continuity had several, freshness had several, and projection could be present or absent. Multiplying them produced combinations the interface should never allow.
The imperative version had hidden those combinations in scattered flags. React would happily render any combination I passed.
I replaced some independent booleans with constrained states. continuity: current required a projection through a named revision. recovering carried an attempt identity and last-known projection. unobserved meant no source evidence had ever arrived and could not coexist with a displayed current count.
The derived presentation handled legitimate cross-products and rejected impossible ones in development. A feed could be transport-open and source-stale; that partial failure was real. It could not be continuity-current with a known revision gap.
The exercise made a broader point: explicit state can first make a design feel more complex because it stops hiding contradictions. The answer is not to return to booleans. It is to encode the valid relationships and choose where uncertainty belongs.
React provided pressure to hand the view a coherent value. The model still had to earn coherence.
Long-running behavior remained the test
The prototype looked correct in short interactions and initially leaked one subscription on every remount. The component lifecycle made cleanup possible, but it did not write the cleanup for me.
Because transport ownership had moved outside the view, the final component subscription was to the session controller, not directly to the socket. Mount registered one listener; unmount removed the same function reference. An endurance fixture repeatedly mounted, unmounted, and changed configuration while checking that listener counts returned to baseline.
The clock controller also needed one shared schedule rather than one interval per status label. React made it easy to create many components; careless ownership could multiply background work just as easily as the earlier imperative code.
I compared event-application work over a day-shaped stream. Rendering from state simplified correctness, while bounded history and stable component identity still determined whether performance stayed flat.
The framework did not replace the lifecycle lessons from the long-running tab. It gave them clearer places to live.
I adopted the explicit state boundary, pure transition functions, derived presentation, and exclusive ownership of rendered regions immediately. Those ideas improved the existing code whether or not React shipped.
I kept React in a prototype until its transfer and tooling costs made sense for more than one panel. C62Y's performance budget had made me wary of adding a library because its programming model felt exciting. The reference device still had to parse and execute it.
I also deferred a large component abstraction layer. The project needed a few concrete feed views before it could know which patterns were genuinely shared. Reusing markup was less important than preserving the state distinctions.
This was not skepticism about React's future. It was skepticism about confusing technology adoption with completion. The most valuable result of the experiment was a model that could outlive the experiment.
The decision left T04P in a hybrid state for a while. That was acceptable because the boundary was explicit and the hard data rules remained outside both rendering systems.
The durable part was the question
React's public arrival gave me a practical way to ask a question I had been avoiding: if the page were rendered fresh from one value, what would that value need to contain?
The answer included much more than the latest count. It included source evidence, observation time, transport state, continuity, active recovery identity, bounded history, and the current time used for classification. Writing that answer exposed contradictions the DOM had been storing silently.
React made the view a function of state. It did not make socket state into source health, close revision gaps, decide stale policy, or assign ownership to asynchronous results. Those remained product and protocol work.
That distinction protected the project from two opposite mistakes. I did not dismiss the framework as “just a view library,” because its constraints improved how I structured the interface. I did not credit it with solving the reliability model it merely displayed.
The framework was interesting. The habit that lasted was asking whether the state was explicit enough to deserve a deterministic view.
I suspect the important direction is larger than client-side rendering. If a component can declare the information it needs and render from a complete value, some components may eventually be prepared somewhere other than the browser and arrive as useful output with only the interactive parts continuing locally. That would make the ownership question sharper, not obsolete: the system would still need to say which side knows the truth, which code crosses the network, and what remains usable before JavaScript takes over.
I cannot build that architecture from React's first public release, and I would not pretend the current API promises it. The state model makes the possibility legible enough to watch.