React competes with Vanilla Javascript, not with server-side functionality¹.
Example scenario
Consider a simple interactive web application consisting of a slider. When we move the slider, we want some HTML element to move around automatically. This means that we have to find the relevant nodes in the DOM and set up an event listener and update the DOM whenever the state changes:
<div class=container>
<div class=row>
Move me: <input id=slider type=range min=0 max=100>
</div>
<div class=row>
<div id=text>drag the slider to move me</div>
</div>
</div>
const slider = document.getElementById("slider");
const text = document.getElementById("text");
slider.addEventListener('input', ev => text.style.left = `${ev.target.value}vw`);
(live jsfiddle)
This works fine for this simple example, but really doesn't scale well. As multiple interactive elements are added to a page, it's difficult to keep all of the data flows clear and straightforward, without any name clashes and so on.
Reactive user interfaces
Approaches to tame the complexity of GUIs are older than the web, for example with the Model–View–Controller pattern.
What React and similar frontend frameworks like Vue do is to provide a path to tackle this complexity, hiding all the tricky bits. The general idea is that
- the application has some current state,
- the DOM should always be a reflection of the current state, and
- to change the DOM, you change the current state instead.
This is reactive programming, because the output (DOM) always updates itself when its inputs/dependencies (the state) are changed.
This means the programmer does not have to think about all of the DOM × DOM data flows, but can concentrate on managing the state and how to render the state in the current DOM.
A naive implementation of this approach would re-render the entire page/component whenever any part of the state changes:
<div class=container>
<div class=row>
Move me: <input id=slider type=range min=0 max=100>
</div>
<div id=app class=row>
</div>
</div>
const app = document.getElementById("app");
const slider = document.getElementById("slider");
const renderSlidingText = (pos) => app.innerHTML = `
<div id=text style="left: ${pos}vw">
drag the slider to move me
</div>
`;
slider.addEventListener('input', ev => renderSlidingText(ev.target.value));
renderSlidingText(slider.value) // render initial state
(live jsfiddle)
That works mostly fine for this small example, but replacing larger parts of the DOM would degrade performance noticeably (layouting the page is expensive for the browser). So what these frameworks do is to avoid using Element.innerHTML
, instead parse the template themselves, and then compare the desired DOM with the current DOM to apply more surgical updates. For example, React would likely see here only the CSS style property needs to be updated, without having to replace the entire element.
User interface components
On top of helping with reactive programming and efficient DOM updates, these libraries also provide ways to build reusable components. For example, the following React example uses a <SlidingText>
component:
<div id=app />
const App = () => {
const [pos, setPos] = React.useState(50);
return (
<div className="container">
<div className="row">
<input
type="range" min="0" max="100" value={ pos }
onInput={ (ev) => setPos(ev.target.value) }
></input>
</div>
<div className="row">
<SlidingText pos={ pos } />
</div>
</div>
);
};
const SlidingText = ({ pos }) => (
<div className="text" style={ {left: `${props.pos}vw` } }>
drag the slider to move me
</div>
);
const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<App />);
(live jsfiddle)
Of course, vanilla JS also supports Web Components, but defining them is a bit more effort than a simple function.
Updating the DOM with new data
You do mention Ajax requests to load new content into the current page, but unless you directly load HTML and use the Element.innerHTML
approach you're going to have to manipulate the DOM somehow. For example, assume our backend gives us a list of strings ["a", "b", "c"]
.
With vanilla JavaScript we'd have to resort to some serious contortions like
<ul id=data>
</ul>
const list = document.getElementById("data");
const renderData = (data) => {
list.innerHTML = ''; // delete old contents
for (const item of data) {
const li = document.createElement("li");
li.innerText = item;
list.appendChild(li);
}
};
But with React, we can just declaratively map the data to a desired DOM:
const List = ({ data }) => (
<ul>
{ data.map(item => <li>{ item }</li>) }
</ul>
);
And if we give each list item a unique key
, then React's merging/reconciliation algorithm can handle subsequent updates of the input list more efficiently, without having to rewrite the entire list's DOM when elements are inserted/deleted.
Conclusion
Is all this extra effort worth it? Not for simple examples like a single slider. But React-style state management and rendering gets quite desirable for single-page applications, highly dynamic frontends, or when you want multiple instances of the same interactive component.
Of course, not every website is highly interactive. In a lot of cases, rendering templates on the backend is perfectly fine. Replacing partial content with Element.innerHTML
is totally OK for a lot of use cases. For example, dynamically loading HTML fragments is quite popular and successful in the Rails community with frameworks like Turbolinks/Turbo/Hotwire. For example, the GitHub web interface uses this approach for its major navigation.
¹ though React is sometimes used for server-side rendering, a use case that competes with traditional template engines.