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.

3 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

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.

Controlled vs. uncontrolled inputs, single source of truth, React's reconciliation model.