Prior to React Conf 2018, functional components were stateless, and inherited their capabilities from a parent component that was extended off of the React Component class. These components purely inherited and displayed data.
In a world where hooks and the context API exist, the class-based React component is becoming a dinosaur, and for good reason.
The intent of this article is two-fold — first, to persuade you, as a developer, to shift toward writing functional components moving forward, and if you are already utilizing functional components, to perhaps strengthen your resolve to continue to choose them over class-based components — and second, to allow you to take this knowledge and apply it immediately in the real world.
“this” is Too Much
When working within a class-based component, props, state, refs, and methods all are referenced on the class using the this keyword. As someone who has worked with this (pardon the pun) quite a bit, using this constantly can get repetitive.
When using methods within your class-based component, you may often find yourself having to use arrow functions or binding a function to the class’ this context. Although this is standard practice to retain a reference to essentials such as this.state and this.setState, wouldn’t it be better to be able to directly reference the state variable and set the state without worrying about this at all?
In the following sections, we’ll see how functional components avoid altogether this issue by example. Had enough of this yet?
Intuitive State Handling
You don’t have to follow the “setX” nomenclature standard either. Call your setter function “updateText”. Although I’d recommend for the most part using the “setX” standard for writing your setter functions, I could imagine cases other verbiage could be more descriptive.
BUT WAIT! What about if I want to use preexisting state? Well, have no fear. The setter function returned by the useState hook also accepts function, where the first parameter is the previous state.
Awesome, we’ve utilized useState. Let’s add a POST function to persist our data now, it looks like super important stuff we’d want to save for our user.
If you are familiar with optimizing React components, you may have noticed a minor issue in the above example — the function passed to the onClick handler of the ‘POST to Server’ button isn’t memoized and will be recreated every render. Although in this small of an example, the effects of this aren’t noticeable to the end user, this is a good place to bring up a panacea to this issue — our next hook, useCallback.
Let’s see the useCallback hook in action by optimizing our previous example before divulging into the details.
Not too crazy, but what is going on here? The first argument being passed into the useCallback hook is our function to memoize. We’ll be referencing two of our state variables within this function, number and text. As such, we pass them into our dependency array, the second argument to useCallback.
Now, our function will only be re-memoized once those two values have changed. React will keep this function from being recreated every time the component is re-rendered until then.
One thing to realize here is that useCallback memoizes our function, but won’t memoize a value returned from the function being called. How can we memoize a value? Let’s introduce our next hook, useMemo.
The useMemo hook is very similar to the useCallback, the difference being useMemo memoizes values. Let’s see it in action.
You will typically use useMemo to memoize values that are too expensive to reevaluate every re-render of the component. This is discretionary to the developer as to when is the best place to use this hook.
Functional Component Lifecycles
Everything looks in place in our examples — we’ve successfully managed our state and optimized our renders using our functional components. There’s one thing you may notice missing: lifecycles.
Using hooks, we can accomplish what React’s predefined lifecycles do for us in a much more readable and granularly focused way. We are truly in control of how our component reacts to change.
The useEffect hook is how we approach lifecycles in our functional components. It in its’ simplicity encompasses the capacity to replace entirely (almost, but we’ll get there) our traditional class-based React component lifecycle methods.
Effects are a concept that are unique to hooks, and although can function to replace our traditional class-based lifecycle methods, are not the same. They in turn offer a new and intuitive way to control your component at an incredibly granular level. With useEffect utilized correctly, you’ll be in full control.
You can have as many effects within your component as needed. Let’s take a look at how we can use useEffect within a functional component to give us the same result as using componentDidMount within a class-based component.
What’s happening above? The effect is looking for items in the dependency array to “depend” on to fire. As we’ve provided an empty array, this effect will fire once when the component is mounted, and not again, as we have provided an empty dependency array. So the question arises — why must we pass an array at all if it is empty? Why not just not provide a second argument to the useEffect hook?
By not providing a second argument, we are telling the effect the exact opposite of run once — we’re telling it to run every time there’s a re-render and disregard dependencies.
With this in mind, I can’t disregard one of the biggest use cases of componentDidMount, API calls! Intuitively, you may do the following:
My VSCode is smart enough to tell this isn’t correct. It’s learned well! Effect callbacks are synchronous to prevent race conditions. Does this mean we can’t do asynchronous things within them? Nope! Let’s see how.
In the above example, we could write either of the above effects (both are the same thing, written different ways). Personally, I prefer the first variation.
Let’s next talk about componentWillUnmount. This is a very common lifecycle used to remove listeners or intervals created by a class-based component. The useEffect hook’s first argument, the function, can optionally return a cleanup function that performs this unsubscribing.
In this example, we are adding an event listener on mount. On unmount of the Orphan component, we have provided a callback function to the effect to be fired which removes the event listener.
The componentDidUpdate lifecycle returns to us previous props and states to work with. This lifecycle is usually one of the most irritating to work with, as we have to create numerous if conditions to fire side effects to state and prop updates. Using hooks, this problem is alleviated altogether, we simply declare an effect which essentially subscribes to the props and state passed in as dependencies. One issue we encounter here is that we don’t have access to previous prop or state values, and to be frank, there’s no in house solution to this offered by React.
I typically rely on a custom hook to replicate this functionality, and to great ease. The usePrevious hook is a community standard hook that is not in house to React. There are packages available to offer this functionality, but I prefer to just create a utils hook folder where I place the custom hooks I require in my project where I would copy the usePrevious code. This is part of the reason I stated earlier React hooks almost replaces the class-based React component — it actually does, with help.
The next lifecycle method, shouldComponentUpdate, is actually one of the more less reliable to prevent component updates. The new standard for memoizing components to prevent re-renders unnecessarily is not a hook at all, its actually the React.memo function.
Unfortunately, there is no in house API that replaces componentDidCatch, although the React team has stated this is in the works. In the mean time, I’d suggest using the react-error-boundary package. The syntax also is cleaner than using componentDidCatch.
There are a few more hooks and advantages that haven’t been covered in the scope of this article, notably the useReducer hook for Redux fans inbuilt to React.
I hope I’ve been able to show you that there are new ways to write React that could potentially make life a lot easier and code behave more in line with what your mental model for the behavior to be like.
All the best, happy React-ing.