Skip to content

From jQuery to React: what finally made components click

· 3 min read · Amrith Vengalath

  • React
  • JavaScript
  • Web

For a long time my mental model of a web page was: the HTML is the source of truth, and JavaScript's job is to reach in and change it. Need to show an error? Find the div, set its text, remove the hidden class. jQuery was perfect for that - $('#error').text(msg).show() and you're done.

React broke that model on purpose, and it took me a while to stop fighting it.

The thing I kept getting wrong

My first React components were just jQuery with extra steps. I'd try to "update the DOM" inside a component, reach for refs constantly, and wonder why everything felt awkward. I was still thinking do this to the page.

The shift that fixed it: in React you don't change the page. You describe what the page should look like for a given state, and React figures out the DOM changes. You change the data, and the UI follows.

So the error message isn't something you show and hide. It's something that's either in your state or not, and the UI is a function of that:

function LoginForm() {
  const [error, setError] = useState("");
 
  async function handleSubmit(e) {
    e.preventDefault();
    try {
      await login(/* ... */);
    } catch (err) {
      setError("Wrong email or password");
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      {/* inputs */}
      {error && <p className="error">{error}</p>}
    </form>
  );
}

There's no "show the error" line. There's setError(...), and the {error && ...} describes the consequence. I update the state; the rendering takes care of itself. Once that landed, a lot of code I used to write just disappeared.

State is the part that matters

In jQuery, state lived in the DOM. Is the menu open? Check if it has the open class. Which tab is active? Look at which one has active. The DOM was the state, which is fine until two things disagree about it and you're hunting a bug where the class says one thing and the data says another.

React pushes you to keep state in one place and let the DOM be a reflection of it:

const [activeTab, setActiveTab] = useState("profile");
// the buttons set it, the panel reads it - one source of truth

The DOM can never silently disagree with your state, because the DOM is generated from the state every render. That alone removed a whole category of bugs I used to live with.

What tripped me up

A few honest stumbles from those first weeks:

  • I mutated state directly (items.push(x)) and couldn't figure out why nothing re-rendered. You have to create a new value: setItems([...items, x]). React compares references.
  • I reached for useRef and direct DOM access way too often, out of old habit. Most of the time I didn't need it - I needed to rethink what state drove the thing.
  • I overcomplicated everything into giant components before learning to split them. Smaller components that each own a small piece of state are far easier to reason about.

Was it worth leaving jQuery?

For a small static page with a bit of interactivity, jQuery is still genuinely fine, and I won't pretend otherwise. But the moment a UI has real state - forms, lists that change, things that depend on each other - the "describe the UI from state" model scales so much better than "reach in and poke the DOM." Code I'd have dreaded touching in jQuery became boring and predictable in React, and boring is exactly what you want in code you have to maintain.

If you're making this jump and it feels clumsy: you're probably still writing jQuery in React's clothing. Stop thinking about changing the page. Start thinking about what state the page is in, and let the rendering be the output.