React was easy to reason about

When React first burst onto the scene, its primary innovation was the Virtual DOM: instead of having to manually make sure the DOM is in sync with the actual application state, you'd just recreate a representation of the full DOM on every state change, and React would then update the actual DOM with exactly the changes needed to make it match. React components were simply a function of the application state, and the main benefit of that, as was the slogan at the time, is that it makes them easy to reason about. We were now able to create highly interactive client-side applications without introducing countless subtle bugs that were hard to catch.

Although that enabled way richer client-side applications than before, all that logic could only be executed once the code was downloaded and parsed by the user's browser, with nothing bug a blank page for the user to stare at. So we started doing Static Site Generation: do the first render of the virtual DOM at build time, and use that to send the user some HTML that can be shown while waiting for the client-side code to run. Of course, since that first render is done on the server side, you can't use client-side APIs in there, so make sure to keep in mind what environment the code you're writing might be running in. For example, you could set a state variable as a side-effect, and then wrap your code in a check for that variable.

One downside is that SSG happens at build-time, and thus only works for HTML that looks the same for every user. So we came up with tricks to customise the initial payload at runtime: Server-Side Rendering. Often implemented as separate APIs which only run on the server-side, and thus can access server-side APIs and secrets to fetch the relevant data for the user, and then pass those as props to your components. Of course, if that data is updated in the lifecycle of the client-side application, then that will have to make sure that the updated data is shown instead of the initial data passed as props.

With SSR we could now always send useful HTML while the user was waiting for the client-side code to have run, but we can't start sending that until we've collected all the data a page needs, no matter how unimportant. Besides, it was bolted on top of React through proprietary APIs specifc to the different frameworks that implemented it. So we got server components in React itself, which can be rendered completely on the server, sent as soon as they're ready, and don't have to (virtually) be rendered again on the client-side, thus no longer delaying the client-side interactivity.

These server components look similar to traditional, now-known-as client components, can access async and server-side APIs, but they cannot access client-side APIs, nor state and effects. Client components will usually also render on the server, so also make sure not to access client-side APIs in their first render. Also, you can have individual functions inside client components call server-side APIs if they're submit event handlers for forms, or wrapped in a "transition", and have "use server" at the top of the function body. Components are server components by default, but become client components if they have "use client" at the top. Well, or if one of their parent components has that, so make sure you to check where your component gets used before accessing server- or client-side APIs, or avoid both altogether.

Is React too complex?

Well… In theory, the changes have been strictly additive: it is still perfectly possible to build an app that is fully rendered on the client side, and the view is mostly a function of state. What has changed, is our demands: we want the first-load performance of traditional, fully server-rendered websites where possible, combined with the reliable highly-interactive client-side experiences where needed. It's possible that exercising such granular control just comes with inherent complexity, and React is the best we can do in terms of managing that.

But maybe not?


React