The Missing Introduction to React

Why React is the Top UI Framework in the World

Eric Elliott
JavaScript Scene
12 min readSep 3, 2020

--

React is the world’s most popular JavaScript framework, but it’s not cool because it’s popular. It’s popular because it’s cool. Most React introductions jump right into showing you examples of how to use React, and skip the “why”.

That’s cool. If you want to jump in and start playing with React right away, the official documentation has lots of resources to help you get started.

This post is for people who want an answer to the questions, “why React? Why does React work the way it works? What is the purpose of the API designs?”

Why React?

Life is simpler when UI components are unaware of the network, business logic, or app state. Given the same props, always render the same data.

When React was first introduced, it fundamentally changed how JavaScript frameworks worked. While everyone else was pushing MVC, MVVM, etc, React chose to isolate view rendering from the model representation and introduce a completely new architecture to the JavaScript front-end ecosystem: Flux.

Why did the React team do that? Why was it better than the MVC frameworks (and jQuery spaghetti) that came before?

In the year 2013, Facebook had just spent quite a bit of effort integrating the chat feature: A feature that would be live and available across the app experience, integrating on virtually every page of the site. It was a complex app within an already complex app, and uncontrolled mutation of the DOM, along with the parallel and asynchronous nature of multi-user I/O presented difficult challenges for the Facebook team.

For instance, how can you predict what is going to be rendered to the screen when anything can grab the DOM and mutate it at any time for any reason, and how can you prove that what got rendered was correct?

You couldn’t make those guarantees with any of the popular front-end frameworks prior to React. DOM race conditions were one of the most common bugs in early web applications.

Non-determinism = parallel processing + mutable state” — Martin Odersky

Job #1 of the React team was to fix that problem. They did that with two key innovations:

  • Unidirectional data binding with the flux architecture.
  • Component state is immutable. Once set, the state of a component can’t be changed. State changes don’t change existing view state. Instead, they trigger a new view render with a new state.

“The simplest way that we have found, conceptually, to structure and render our views, is to just try to avoid mutation altogether.” — Tom Occhino, JSConfUS 2013

With flux, React tamed the uncontrolled mutation problem. Instead of attaching event listeners to any arbitrary number of arbitrary objects (models) to trigger DOM updates, React introduced a single way to manipulate a component’s state: Dispatch to a store. When the store state changes, the store will ask the component to re-render.

Flux architecture

When I’m asked “why should I care about React”, my answer is simple: Because we want deterministic view renders, and React makes that a lot easier.

Note: It is an anti-pattern to read data from the DOM for the purpose of implementing domain logic. Doing so defeats the purpose of using React. Instead, read data from your store and make those choices prior to render-time.

If deterministic render was React’s only trick, it would still be an amazing innovation. But the React team wasn’t done innovating. They launched with several more killer features, and over the years, they’ve added even more.

JSX

JSX is an extension to JavaScript which allows you to declaratively create custom UI components. JSX has important benefits:

  • Easy, declarative markup.
  • Colocated with your component.
  • Separate by concern, (e.g., UI vs state logic, vs side-effects) not by technology (e.g., HTML, CSS, JavaScript).
  • Abstract away DOM differences.
  • Abstract away from underlying tech so you can target many different platforms with React. (e.g., ReactNative, VR, Netflix Gibbon, Canvas/WebGL, email, ...)

Prior to JSX, if you wanted to write declarative UI code, you had to use HTML templates, and there was no good standard for it at the time. Every framework used their own special syntax you had to learn to do things like loop over data, interpolate variables, or do conditional branching.

Today, if you look at other frameworks, you still have to learn special syntax like the *ngFor directive from Angular. Since JSX is a superset of JavaScript, you get all of JavaScript’s existing features included in your JSX markup.

You can iterate over items with Array.prototype.map, use logic operators, branch with ternary expressions, call pure functions, interpolate over template literals, or generally anything else a JavaScript expression can do. In my opinion, this is a huge advantage over competing UI frameworks.

There are a couple rules you may struggle with at first:

  • The class attribute becomes className in JSX.
  • For every item in a list of items you want to display, you need a stable, unique identifier to use for the JSX key attribute. The key must not change when items are added or removed. In practice, most list items have unique ids in your data model, and those usually work great as keys.

React didn’t prescribe a single solution for CSS. You can pass a JavaScript style object to the style property, in which case, many common style names are converted to camelCase for the object literal form, but there are other options. I mix and match a couple different solutions, depending on the scope I want for the style I’m applying: global styles for theming and common layouts, and local scoped for this component only.

Here are my favorite options:

  • CSS files can be loaded in your page header for common global layouts, fonts, etc. They work fine.
  • CSS modules are locally scoped CSS files that you can import directly in your JavaScript files. You’ll need a properly configured loader. Next.js enables this by default.
  • styled-jsx lets you declare styles inline in your React components, similar to how <style> tags work in HTML. The scope for those styles is hyper-local, meaning that only sibling tags and their children will be affected by the styles. Next.js also enables styled-jsx by default.

Synthetic Events

React provides a wrapper around the DOM events called synthetic events. They are very cool for several reasons. Synthetic events:

  1. Smooth over cross-platform differences in event handling, making it easier to make your JS code work in every browser.
  2. Are automatically memory managed. If you were going to make an infinitely scrolling list in raw JavaScript + HTML, you would need to delegate events or hook and unhook event listeners as elements scroll on and off the screen in order to avoid memory leaks. Synthetic events are automatically delegated to the root node, meaning React developers get event memory management for free.

Note: Prior to React v17, it’s not possible to access synthetic event properties in asynchronous functions because of event pooling. Instead, grab the data you need from the event object and reference it in your closure environment. Event pooling was removed in v17 because browser optimizations take care of it.

Note: Prior to v17, synthetic events were delegated to the document node. After v17, synthetic events are delegated to the React root node.

Component Lifecycle

The React component lifecycle exists to protect component state. Component state must not be mutated while React is drawing the component. Instead, a component gets into a known state, draws, and then opens up the lifecycle for effects, state updates, and events.

Understanding the lifecycle is key to understanding how to do things the React way, so you won’t fight with React, or accidentally defeat the purpose of using in the first place by improperly mutating or reading state from the DOM.

Beginning at React 0.14, React introduced class syntax to hook into React’s component lifecycle. React has two different lifecycles to think about: Mounting, Updating, and Unmounting:

React Lifecycle

And then within the update lifecycle, there are three more phases:

React Update Cycle
  • Render — aside from calling hooks, your render function should be deterministic and have no side-effects. You should usually think of it as a pure function from props to JSX.
  • Pre-Commit — Here you can read from the DOM using the getSnapShotBeforeUpdate lifecycle method. Useful if you need to read things like scroll position or the rendered size of an element before the DOM re-renders.
  • Commit — During the commit phase, React updates the DOM and refs. You can tap into it using componentDidUpdate or the useEffect hook. This is where it’s OK to run effects, schedule updates, use the DOM, etc.

Dan Abramov made a great diagram that spells out all the details as you might see it from the React class perspective:

React Component Lifecycle Diagram by Dan Abramov (Source)

In my opinion, thinking of a component as a long-lived class is not the best mental model for how React works. Remember: React component state is not meant to be mutated. It’s meant to be replaced, and each replacement of the current state triggers a re-render. This enables what is arguably React’s best feature: Making it easy to create deterministic view renders.

A better mental model for that behavior is that every time React renders, it calls a deterministic function that returns JSX. That function should not directly invoke its own side effects, but can queue up effects for React to run.

In other words, you should think of most React components as pure functions from props to JSX.

A pure function:

  • Given same inputs, always returns the same output (deterministic).
  • Has no side-effects (e.g., network I/O, logging to console, writing to localStorage, etc.)

Note: If your component needs effects, use useEffect or call an action creator passed through props and handle the effects in middleware.

React Hooks

React 16.8 introduced a new concept: React hooks are functions that allow you to tap into the React component lifecycle without using the class syntax or directly calling lifecycle methods. Instead of declaring a class, you write a render function.

Calling a hook generally introduces side-effects — effects which allow your component to hook into things like component state and I/O. A side-effect is any state change observable outside the function other than the function’s return value.

useEffect lets you queue up effects to run at the appropriate time in the component lifecycle, which can be just after the component mounts (like componentDidMount), during the commit phase (like componentDidUpdate), or just before the component unmounts (like componentWillUnmount).

Notice how three different lifecycle methods fell out of a single React hook? That’s because instead of putting logic in lifecycle methods, hooks allow you to keep related logic together.

Many components need to hook something up when a component mounts, update it every time the component re-draws, and then clean up before the component unmounts to prevent memory leaks. With useEffect, you can do that all in one function call, instead of splitting your logic into 3 different methods, mixed with all the other unrelated logic that also needs to use those methods.

Hooks enable you to:

  • Write your components as functions instead of classes.
  • Organize your code better.
  • Share reusable logic between different components.
  • Compose hooks to create your own custom hooks (call a hook from inside another hook).

Generally speaking, you should favor function components and React hooks over class-based components. They will usually be less code, better organized, more readable, more reusable, and more testable.

Container vs Presentation Components

For better modularity and reusability of components, I tend to write my components in two parts:

  • Container components are components that are connected to the data store and may have side-effects.
  • Presentation components are mostly pure components, which, given the same props and context, always return the same JSX.

Tip: Pure components should not be confused with React.PureComponent, which is named after pure components because it’s unsafe to use it for components that aren’t pure.

Presentation components:

  • Don’t touch the network
  • Don’t save or load from localStorage
  • Don’t generate random data
  • Don’t read directly from the current system time (e.g., by calling a function like Date.now())
  • Don’t interact directly with the store
  • May use local component state for things like form inputs, as long as you can pass in an initial state so that they can be deterministically tested

That last point is why I call presentation components “mostly pure”. Once React takes control of the lifecycle, they’re essentially reading their component state from React global state. So hooks like useState and useReducer provide implicit data input (input sources that are not declared in the function signature) making them technically impure. If you want them to be really pure, you can delegate all state management responsibility to the container component, but IMO, it’s overkill as long as your component is still unit testable.

“Perfect is the enemy of good” — Voltaire

Container Components

Container components are components which handle state management, I/O, and any other effects. They should not render their own markup — instead, they delegate rendering to the presentation component they wrap. Typically, a container component in a React+Redux app would simply invoke mapStateToProps, mapDispatchToProps, and wrap the presentation component with the result. They may also compose in many cross-cutting concerns (see below).

Higher Order Components

A Higher Order Component (HOC) is a component which takes a component and returns a component in order to compose in additional functionality.

Higher Order Components work by wrapping a component around another component. The wrapping component adds some DOM or logic, and may or may not pass additional props into the wrapped component.

Unlike React hooks and render props components, HOCs are composable using standard function composition, so you can declaratively mix in shared behavior across all your app components without those components knowing that those behaviors exist. For example, here is an HOC from EricElliottJS.com:

This mixes in all the common, cross-cutting concerns shared by all the pages on EricElliottJS.com. withEnv pulls in environment settings, withAuth adds GitHub authentication, withLoader displays a spinner while user data is loading, withLayout({ showFooter: true }) displays our default layout with a footer at the bottom of the page, withFeatures loads our feature toggle settings, withRouter loads our router, withCoupon handles magic coupon links, and withMagicLink handles our passwordless user authentication with Magic.

Tip: Passwords are obsolete and dangerous. Nobody should be writing new apps with password authentication today.

Almost all the pages on our site use all of those features. With this composition done in a higher order component, we can compose it into our container components with one line of code. Here’s what that would look like for our lesson page handler:

import LessonPage from '../features/lesson-pages/lesson-page.js';
import pageHOC from '../hocs/page-hoc.js';
export default pageHOC(LessonPage);

A common but miserable alternative to these kinds of HOCs is the pyramid of doom:

Repeat for every page. If you need to change this anywhere, you have to remember to change it everywhere. It should be self-evident why this sucks.

Leveraging composition for cross-cutting concerns is one of the best ways to reduce code complexity in your applications. The topic of composition is so important, I wrote a whole book on it: “Composing Software”.

Recap

  • Why React? Deterministic view renders, facilitated by unidirectional data binding, and immutable component state.
  • JSX provides easy, declarative markup in your JavaScript.
  • Synthetic events smooth over cross-platform events and reduce memory management headaches.
  • The component lifecycle exists to protect component state. It consists of mounting, updating, and unmounting, and the updating phase consists of render, pre-commit, and commit phases.
  • React hooks allow you to tap into the component lifecycle without using the class syntax, and also make it easier to share behaviors between components.
  • Container and Presentation Components allow you to isolate presentation concerns from state and effects, making both your components and business logic more reusable and testable.
  • Higher Order Components make it easy to share composable behaviors across many pages in your app in a way that your components don’t need to know about them (or be tightly coupled to them).

Next Steps

We touched on a lot of functional programming concepts in this simple React introduction. If you really want to understand how to build React applications, it’s a good idea to reinforce your understanding of concepts such as pure functions, immutability, curried functions, partial application, and function composition. These topics are covered with video and code exercises on EricElliottJS.com.

I recommend paring React with Redux, Redux-Saga and RITEway. I recommend pairing Redux with Autodux and Immer. For complex state transitions, check out Redux-DSM.

When you’ve got the foundations down and you’re ready to build real apps with React, Next.js and Vercel can automate the process of setting up your build configuration, CI/CD, and highly optimized, serverless deployment. It’s like having a full time DevOps team, but it actually saves you money instead of costing you full-time salaries.

Eric Elliott is a tech product and platform advisor, author of “Composing Software”, cofounder of EricElliottJS.com and DevAnywhere.io, and dev team mentor. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.

He enjoys a remote lifestyle with the most beautiful woman in the world.

--

--