There was a time when creating a web page meant creating an html file, yet nowadays it seems impossible to build any frontend without the bottomless pit of node_modules, yielding a finely chewed yet hefty bundle.xyz.js. Well, I got to learn that it might not be the case soon and, naturally, I feel the urge to share it with the rest of you.
[read more]Me & React: 5 Years in 15 Minutes
I first heard of React when a friend showed me a project he had written. It was some sort of content management system: it had tables, forms, visual editors and stuff like this. I don't remember much, except that code was really really confusing, it looked like this:
// hint: you can use this codepen to follow along:
// https://codepen.io/valeriavg-the-flexboxer/pen/WNJNMRp
const app = React.createElement(
// tag
"button",
// properties
{
onClick: function () {
alert("Hello!");
},
},
// children
"Click Me!"
);
ReactDOM.render(app, document.getElementById("app"));
// <div id="app"></div>
So, of course, I was quite frustrated someone would want to write that, when you could just write this instead:
<button onClick="alert('Hello!')">Click me!</button>
JSX: HTML in JS
Some time passed, and, to my surprise, React was all over the place: every other job advertisement was mentioning it.
And so I gave it another try. This time around it wasn't just a library you import - somehow it turned into a whole new language, called jsx. Which, however, was vaguely familiar:
const app = <button onClick={() => alert("Hello, JSX!")}>Click me!</button>;
ReactDOM.render(app, document.getElementById("app"));
That was almost the same as my old pal HTML, except JSX allowed splitting HTML pages into tiny reusable dynamic building blocks:
const One = () => <div> One </div>;
const Two = () => <div> Two </div>;
const Three = () => <div> Three </div>;
const app = (
<div>
<One />
<Two />
<Three />
</div>
);
ReactDOM.render(app, document.getElementById("app"));
Behind the scenes, however, it was still the same code:
const One = () => React.createElement("div", {}, "One");
const Two = () => React.createElement("div", {}, "Two");
const Three = () => React.createElement("div", {}, "Three");
const app = React.createElement("div", {}, One(), Two(), Three());
ReactDOM.render(app, document.getElementById("app"));
But JSX made a lot of difference and React has finally started making sense to me.
Stateful and stateless components
One of the first things I've learned was a "stateful" component:
class App extends React.Component {
constructor() {
super();
this.state = {
name: "",
};
}
render() {
return (
<div>
<h1>Hello, {this.state.name} </h1>
<input
type="text"
value={this.state.name}
onChange={(e) => this.setState({ name: e.target.value })}
/>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("app"));
Or rather it was the second thing, as, apparently, I've already got familiar with it's "stateless" counterpart.
A stateful component had a state which was triggering a re-render on change, while the stateless had only the render part and were rendering exactly the same thing as long as props were the same:
class App extends React.Component {
render() {
return (
<div>
<h1>Hello, {this.props.name}! </h1>
</div>
);
}
}
ReactDOM.render(<App name="React" />, document.getElementById("app"));
Back then I worked for a startup which allowed creators distribute video content for their fans among other things. That meant we had a dashboard for creators, and website and an app for end users. And React worked perfectly well for the dashboard, especially when functional components came along:
const App = () => {
const [name, setName] = React.useState("");
return (
<div>
<h1>Hello, {name} </h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
And so, the project was growing, along with the amount of dependencies. We used emotion styled components, react-router and of a little something to manage the state.
State management
One of the first libraries for state management I've tried was RxJS. Sure, it added even more magic to the project, but hey, it was cool to share state between two components, right?
Wrong, it was chaos! I could never tell which one of them changed the state and it made debugging quite mind bending, as sometimes console.log
has been printed a microsecond before the state has been actually propagated.
Redux has treated me a bit better in that sense, but having one gigantic store was not convenient for my preferred modular architecture.
And so I stuck to the React's own context
because I could easily split states and trace the updates easier:
const NameContext = React.createContext("");
const Name = () => {
const name = React.useContext(NameContext);
if (!name) return "";
return <h1> Hello, {name}!</h1>;
};
const App = () => {
const [name, setName] = React.useState("");
return (
<NameContext.Provider value={name}>
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="What's your name?"
/>
<Name />
</div>
</NameContext.Provider>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
Which, as you can tell from the code, was precisely around the time functional components came along.
Functional components
In a nutshell, functional components were an attempt to turn stateful components into stateless with ease and vice versa by letting them all be functions and use hooks instead:
const App = () => {
const [name, setName] = React.useState("");
return (
<div>
<h1>Hello, {name} </h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
I can't argue that code became much easier to write and read, though I had my concerns regarding hooks. First off, the state still needs to be stored somewhere and originally it was proposed to be built around this
, which wouldn't work with arrow functions and would need to rely on the fact that JSX is compiled (not the case as at the moment it uses a dispatcher instead). And secondly, it required thinking in React.
While classes were a mouthful - they were straightforward - there were explicit props
and state
, and when state or the props would change - the render
method would be triggered. And there were a couple of methods you could use to control this flow, like shouldComponentUpdate
or componentDidMount
:
class App extends React.Component {
constructor() {
super();
this.state = {
name: "",
};
}
componentDidMount() {
console.log("Component did mount!");
}
shouldComponentUpdate(props, state) {
console.log({
new: { props, state },
old: { props: this.props, state: this.state },
});
return true;
}
render() {
return (
<div>
<h1>Hello, {this.state.name} </h1>
<input
type="text"
value={this.state.name}
onChange={(e) => this.setState({ name: e.target.value })}
/>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("app"));
Which when turned into a succinct functional component with hooks looked liked magic:
const App = () => {
const [name, setName] = React.useState("");
React.useEffect(() => {
console.log("Mounted!");
}, []);
React.useEffect(() => {
console.log("Name changed:", name);
}, [name]);
return (
<div>
<h1>Hello, {name} </h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
I can't say I know every aspect of how it works now, years after, and back when it was some wild juju I gave up trying to comprehend. Unfortunately, things I don't understand have a tendency to bite me when I least expect it.
Handling performance issues
As I mentioned, React was a working great for our dashboard, and so I've decided to switch our plain old MVC website to a fancy server-side rendered React. That was before NextJS became the de-facto standard for this and I've kinda just glued most pieces together myself: after all, it boiled down to replacing the template engine we were using (I think it was pug) with ReactDOMServer:
//
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const script = React.createElement(
"script",
{},
'console.log("ReactDOM hydrate would happen here")'
);
const page = React.createElement("h1", {}, "Hello, SSR!");
const app = React.createElement("body", {}, page, script);
ReactDOMServer.renderToString(app);
The new version was quite ok and I could add some real reactivity to the otherwise static pages, including the changes to a video player.
I learned that some things required dropping down plain old JS event listeners with the use of refs:
const App = () => {
const videoEl = React.useRef(null);
const [time, setTime] = React.useState(0);
const onTimeUpdate = (e) => {
setTime(e.target.currentTime);
};
React.useEffect(() => {
if (!videoEl.current) return;
videoEl.current.addEventListener("timeupdate", onTimeUpdate);
return () => {
if (!videoEl.current) return;
videoEl.current.removeEventListener("timeupdate", onTimeUpdate);
};
}, [videoEl]);
return (
<div>
<p>{time}s</p>
<video
ref={videoEl}
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
controls
/>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
But, as I discovered after, rendering anything on the screen is an expensive task, let alone a high abstraction like HTML/CSS. Now imagine fetching a video stream, processing it, playing and rendering UI changes every frame with the help of a virtual DOM diffing:
Yup, that's what was happening. Now of course, React was not the main issue - the video processing and playing were the heavy ones, but there were so little resources available and so many optimisation required to make it work properly with React, that I gave up and wrote the player interface is plain JavaScript and just "mounted" it on the React component:
const App = () => {
const videoEl = React.useRef(null);
React.useEffect(() => {
if (!videoEl.current) return;
mountVideo(videoEl.current);
return () => unmountVideo(videoEl.current);
}, []);
return (
<div>
<div ref={videoEl} />
</div>
);
};
const mountVideo = (el) => {
el.innerHTML = `<div class='time'>0s</div><video src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" controls/>`;
el.querySelector("video").ontimeupdate = (e) => {
el.querySelector(".time").innerText = `${e.target.currentTime}s`;
};
};
const unmountVideo = (el) => {
el.innerHTML = "";
};
ReactDOM.render(<App />, document.getElementById("app"));
After this I've build several quick prototypes with the help of GraphQL and React Native and worked on yet another dashboard in React / Redux.
I think by then I finally learned to think in React, but nonetheless from time to time I still turn useEffect
into an endless cycle of updates and forget to memoize stuff.
But I didn't want any of it: I didn't want to learn a language within language with a dozen of libraries, I didn't want to change the way I think - I just wanted to make a performant web application as easy as possible. And I couldn't help but resent React for it.
Yet today I came across a really interesting project, called atomico - a WebComponents library, inspired by React hooks; And it dawned upon me that without React, and, particularly JSX - none of it would be possible, not my beloved svelte, not flutter, nor dozens of other frameworks.
And just as I'd advise any web developer to learn basics of so-called "vanilla" JavaScript, I highly recommend to look into a library that shaped the modern web technologies, at least out of curiosity.
Hopefully, it'd take you much less time to master than it took me :-)