Typically in the Javascript world, there are more frameworks than there are projects which use those frameworks. Over time, certain frameworks rise up and become the current stars. It’s clear that React is completely dominating our industry and is now the de facto standard for reactive (pun intended) user interfaces.
But where did React come from and why is it so popular?
Luckily, through our close partnership with Facebook, we’ve been around to see the evolution of React from its early stages as an internal library at Facebook all the way to its widespread success today. As Facebook expanded its usage of Javascript across their applications and websites, they evaluated a number of existing open-source frameworks while determining the best route forward for how they structure their Javascript. The common factor they noticed was that frameworks, whether using the MVC (model, view, controller), MVVM (model, view, view model), or MVW (model, view, whatever) patterns, all tended to have the “model” aspect in common. They would use observer patterns to watch the models for changes and mutate the views accordingly. That’s where the difficult part comes into play. Which part of the view do you mutate? What dependencies can cause side effects and mismatched states? In a perfect world, we would be able to throw away the old view and replace it with a brand new view every single time. But that would be prohibitively expensive to do in a browser using Javascript, right? And that’s where React’s virtual DOM was born. By using a virtual representation of the DOM, we really can throw away the old view and create a new one every single time. And a simple comparison between the virtual DOM and the actual DOM shows us exactly what needs to be mutated.
When React was open sourced, it became clear very quickly that the engineers at Facebook who architected React were onto something. Having a library handle views this way can simplify our frontend code and allow us to produce much more declarative components. And thus it rose to the top as the most popular front-end framework.
But as with the first iteration of anything, it came with its quirks.
Class-Based Components
Building components using Javascript classes did a great job of modularizing components and embodying the object-oriented way of doing things. However, it did produce a bit of a learning curve and had some “gotchas” which made React a bit less desirable once an application grew in complexity.
Let’s start with lifecycle methods.
Class-based components have a number of methods, referred to as “lifecycle methods.” These would be inherited from the base React.Component class and would be the preferred way of executing logic and responding to events within the lifecycle of a component.
constructor()
For anyone familiar with one or two object-oriented languages, you may recognize this method. This is the first method that gets called when a class gets instantiated.
componentDidMount()
Now this one is React specific. If you want to run any code once the component is...well, mounted, but not as early as the constructor, this is the method you would use.
render()
The most important of methods, this is where you return the html your component should produce. This method gets run a lot, even if it results in no actual changes to the DOM. This is why the virtual DOM is so important, and this is the key to the “throw away the old and recreate the new” methodology mentioned above.
componentDidUpdate()
This is where you can respond to updates in the component.
The point I’m making here is that the syntax is kind of verbose. And structuring your code cleanly can be difficult when you need to group everything together into a small number of lifecycle methods.
“But Matt,” you may be saying, “why can’t we just have the lifecycle methods call other methods as a means of splitting up the code and making it more readable?” You absolutely can take that approach! But with that comes the issue of context. In a class, you may need to access a property of that class, such as the state. Calling “this.state” is easy enough, but as you nest method calls, the context can change, and suddenly “this” is no longer referring to what you think it’s referring to. The typical approach for addressing the context issue was through binding. Bind the context (“this”) to your method, and it will have an idea of what “this” is referring to. But that can get messy, confusing, or downright ugly.
On the subject of context, there’s another common pitfall that many developers have faced when working with class-based components. There’s a concept called “prop-drilling”. This is when you have nested components that pass data through each other unnecessarily. Let’s say we have 3 components, each nested within the previous component.
ComponentA → ComponentB → ComponentC
If ComponentC needs some data that ComponentA has, ComponentA needs to pass it to ComponentB, and ComponentB needs to pass it to ComponentC. ComponentB doesn’t need that data, so it shouldn’t care or even be aware of that data.
Libraries such as Redux can help solve this problem but tend to be overkill in small cases such as this.
Along Comes Functional Components
Now I don’t mean to disparage class-based components. After all, they were all React was, and React was still a fantastic framework even before functional components came along. But it’s important to note some struggles we had faced to be able to understand how functional components can solve them.
Here’s a comparison between a basic class-based component and a basic functional component.
Class-Based Component
null
Functional Component
null
Now with a component this simple, it’s hard to recognize a major difference between these approaches. So let’s look at a more complicated example from an actual project.
Class-Based
null
Functional
null
Right away, you may notice that not only are there much fewer lines of code, it’s also a lot easier to read. In the class-based example shown above, you’ll see we had to do some binding to ensure that a few particular methods would have access to the context of the class. This wasn’t necessary to do in the functional version due to all the code in a functional component being within the same scope.
Hooks
Let’s talk about hooks. Hooks are a means of accomplishing the same sorts of things we would accomplish with lifecycle methods, but hooks actually enable additional functionality on top of that.
You’ll notice in the functional example presented above, we have a block of code wrapped in “useEffect()”. This is one area where we’re using a hook. Let’s break these down.
useEffect
This hook is where we react to things. And yes, once again that pun is very much intended. Let’s say we want to run some code when a certain prop changes, or when a certain part of the component’s state changes. With this hook, we just say what we want to react to and how we want to react to it. It takes 2 parameters: a function, and an array of things to watch for changes.
useContext
Remember that whole prop drilling situation? This can be easily solved with the Context API. You wrap a higher-level component with a context provider, and any children components, no matter how deeply nested, can leverage that context using this hook. Admittedly, Context was introduced in class-based components, but the functional/hook API is a lot cleaner.
useState
This hook is most likely the one you’ll use most often. No longer are you calling this.setState({ something: “new value” });. Now you’re able to handle state in a much more elegant way.
null
useReducer
This is an alternative to useState, and will be familiar to anyone who has worked with Redux. Essentially, it allows you to route state updates through functions where you can modify state in more complex ways. For example, if you want to increment a number using useState, you would do something like this:
null
With useReducer you can just have the button tell the state to increment, without the button caring about the previous state.
null
At first, this looks a little bit more complicated. But I’ll break it down. The reducer can live in its own file where reducer logic can be isolated. With an example as simple as this, it can be hard to visualize a good use case for useReducer. But as an application becomes more complex, you may want to make the code more DRY (don’t repeat yourself) by throwing common logic into a reducer (especially as state logic begins to grow more complex than just a simple increment action). Then the state logic no longer needs to live within the components themselves.
useRef
This hook is a little more nuanced and flexible, but essentially it allows you to store a [mutable] value that can persist throughout the lifecycle of the component, regardless of re-renders.
The most common use for useRef is to refer to an element in the DOM. This way, you can add event listeners on elements, or read the values of input fields without any sort of onChange or onKeyUp logic.
useMemo
This is a performance shortcut. If you have an expensive operation that you don’t want to re-run every time a render happens, this hook can allow you to only recompute when certain dependencies change. Think of it as a mashup between useState and useEffect, but in a single hook.
Comparing Approaches
While class-based components and functional components differ in their approaches to solving problems, there is still parity in the types of problems you may want to solve. Let’s look at lifecycle methods. We mentioned lifecycle methods above. Now let’s look at how to implement the same functionality in a functional way.
componentDidMount
This one is pretty straightforward. In the dependency array for the useEffect hook, we just don’t pass through any dependencies.
Class Based
null
Functional
null
componentDidUpdate
This one allows us to be much more declarative about what we want to respond to rather than blindly responding to all updates.
Class-Based
null
Functional
null
componentWillUnmount
I’ll be honest, this one isn’t intuitive at first. The first time I had to use this, I wasn’t able to guess what the correct approach would be. But upon some research, I found that there’s actually a pretty clever way to do this. In your useEffect where you do your setup logic, you return a function for running your teardown logic. I find it pretty clever that the setup logic and teardown logic is able to all be in the same place.
Class-Based
null
Functional
null
Summary
Through both the real-world example shown above and through the specific comparisons, we’ve covered our bases and shown the value of using functional components with React hooks. Functional components utilizing React hooks allow us to write cleaner, more declarative code; and we can also write code with fewer side effects and better performance. With React’s popularity, I hope the cleaner syntax of functional components lowers the barrier to entry. I look forward to seeing developers who want to learn more about frontend dive in, and I look forward to seeing people enter the exciting world of development for the first time. Hopefully, React’s popularity will be a part of that.
React is the current standard for frontend development, and functional components are the predominant paradigm, but just wait until we see what comes next...