Moving from PHP to React: What Actually Changes
TIL the hard part of moving from PHP to React isn't JSX syntax — it's unlearning page-per-request thinking and stopping the urge to mutate the DOM yourself.
The Problem #
Karuna Hospital's clinical app started as server-rendered PHP: every action was a form POST, every response was a fresh page. When I rebuilt part of it in React, I kept the PHP mental model and just swapped the templating language — and the UI started breaking in ways that didn't map to any bug I recognized.
Context #
In PHP, state lives in the database and the session; the "current state" of a page is whatever the last request rendered. There's no persistent client-side state to keep in sync — every page load recomputes everything from scratch, so there's nothing to get out of sync.
What I Tried #
I wrote early React components the way I wrote PHP templates: grab a DOM node with a ref, read or write its value directly, and treat JSX as a one-time render pass rather than something that re-runs.
What Went Wrong #
Form fields would show stale values after a submit, or reset unexpectedly on an unrelated re-render. I was mutating input values directly through refs while React's own state also believed it owned those values — two sources of truth fighting for the same DOM node.
The Solution #
Stopped treating components as templates and started treating them as pure functions of state: controlled inputs bound to useState, one source of truth, no direct DOM mutation outside of React's own render cycle.
// Before: refs writing directly to the DOM
inputRef.current.value = newValue;
// After: state is the only source of truth
const [value, setValue] = useState("");
<input value={value} onChange={(e) => setValue(e.target.value)} />Why It Works #
React's reconciliation assumes it's the only thing writing to the DOM between renders. The moment something else mutates a node it's tracking, its diffing gets out of sync with reality, and the next re-render "corrects" the DOM back to whatever state actually says — which looks like your input randomly resetting.
Lessons Learned #
The syntax was never the hard part. The hard part was replacing "state lives in the database, recomputed per request" with "state lives in memory, and the UI is a projection of it that has to stay in sync on every change."
What I Would Do Differently #
I'd have read about controlled vs. uncontrolled components on day one instead of discovering the distinction by debugging a stale form for an afternoon.
Related Concepts #
Controlled vs. uncontrolled inputs, single source of truth, React's reconciliation model.
Related content
Injecting Custom UI Into a Website You Don't Own
TIL that a naive injected panel kept getting wiped out by the host page's own re-renders — the fix was watching for that, not fighting it, and mounting into a container the host page has no reason to touch.
Clinical Management System — Karuna Hospital
A hospital management system built to digitize patient records, appointments, reports, and clinical workflows.
useOptimistic auto-reverts on failure — a hand-rolled optimistic hook doesn't
TIL why React's useOptimistic hook doesn't need explicit rollback code when a server call fails, while a manual optimistic-update hook built on plain useState does.