Converting Class Components to Functional Components

Grant Mathews
Gem Software
Published in
8 min readOct 18, 2022

So, you have some old code written with class components, and you want to bring it up to date? Excellent! Not only are function components often shorter than their equivalent class components, but they’re also often simpler, so they end up having fewer bugs! Additionally, they work with React Fast Refresh, meaning when you use our new Vite build you don’t even have to refresh the page when you make changes to them. The future is now! 🤯 (Side note — it’s worth calling out that this post was taken from an internal doc that we made public. This provides clarity on why we chose this formatting structure!)

Here’s a handy guide for converting class components to function components.

Step 0: Convert to TypeScript first

Is your component written in JavaScript? If so, we encourage the following steps to bring it up to date:

  1. First, convert it from JavaScript to TypeScript (We have an internal guide for this at Gem).
  2. Now, convert it from using class components to using function components.

We encourage doing things in this order. You want to convert to TS (TypeScript) first, so you catch bugs that might happen with the class-to-function component conversion. And you don’t want to do them both at once — engineers in the past have noted that this is challenging, and can lead to long refactors that are difficult to merge back into master.

Alright, everything good? Have a TS class component that needs to be merged? Then let’s begin.

Step 1: Not familiar with hooks?

If you are unfamiliar with hooks, this is a really good guide. Why not go and read it? It’ll save you time in the long run, I promise.

I can wait!

👀📘Don’t worry, I brought a book to read while you finish.

OK, all brushed up on useEffect and useState? Let’s begin!

Step 2: The render method is now the only method.

Apologies for the pictures of code, but Medium doesn’t support side by side code blocks in tables. (By the way, did you know that MacOS today supports copying text directly from images..?)

Step 3: The constructor.

Assignments to this.state should now become the initial value of your state hooks.

👀Sidebar! 👀

I wish I could put this on the actual sidebar. Anyways,

React will attempt to use the initial value that you pass into useState to infer what the type of the variable and the setter is. However, it can only be so smart as what you give it: if you just pass in null, it’s going to think you have a variable that can only be null.

In cases where you want to give a more accurate type, you can pass it in explicitly as a type argument:

const [category, setCategory] = useState<CategoryType | null>(null);

Props

The single argument to the constructor, props, now becomes the single argument to the function. Don’t forget to give it a type!

Other class properties that were not part of state

We can now use useRef for things that used to be refs in class components. A ref is a state in React that does not cause a re-render when you update it. This is perfect for HTML elements.

👀Sidebar! 👀

An extremely common reaction when hearing refs described in this way is something like “Oh shoot, I should have been using refs in a lot of places!” This turns out not to be true in practice! Almost all state in React is involved in rendering in some way. The only state that isn’t is often state that talks to the world outside of React — libraries not written in React, HTML elements, Web Workers, and stuff like that.

(Another common use case for refs is to keep track of the previous value of some state that you have, for perf optimizations and to prevent infinite loops.)

Scrutinize any other state that is stored as a class property extra closely. It almost certainly does not belong. It is almost certainly one of these, ordered by likelihood:

  • State (probably)
  • Just a normal variable, computed from props (likely)
  • A ref (least likely)

Here’s an example of a property that should be a ref. I can tell it should be a ref because it is state, but, logically, updating the typeahead client should not cause a re-render.

Step 4: componentDidMount

The common advice given by StackOverflow (SO) is that you can convert componentDidMount code into a useEffect that has no dependency array. And yes, this will work, but it’s missing some nuance.

Just to anchor you: in day-to-day code I write, I almost never use useEffect with an empty dependency array: it’s probably less than 5% of all useEffects that I write. How can that be?

One of the ways that your thinking will shift as you start writing function components is that you will stop thinking about “when the entire component does something (updates, mounts, etc)” and you’ll start thinking about “when a value changes”. So, scrutinize your componentDidMount code extra closely. Would it ever make sense to re-run this code? If so, the “useEffect with an empty dependency array” strategy might not make sense.

Cases where we don’t want to useEffect(…, []) and call it a day

Let me provide an example.

Say you have some code that fetches a user’s profile. Normally, with class components, you’d write code to fetch the profile in componentDidMount. So you’d follow SO and write a useEffect with an empty dependency array. But is it actually true that the profile would never change?

Actually, it’s not! What if the user id passed in changes? Then you’d definitely have to refetch the profile.

And perhaps you’re thinking, well, that’s dumb! I would never change the user id. But the point is now you never even have to think about it. In class-based components, if you later decided you needed to update the props of this component, you’d have to go hack out some code in componentWillReceiveProps. (I remember doing this, and it was far too easy to inadvertently cause errors by forgetting to cover a case.) Now, when you write code with hooks, you never have to do this: all your code automatically has the property that it doesn’t become stale if your dependencies change.

I don’t even have to type the componentWillReceiveProps bit. It’s done for me.

Cases where useEffect(…, []) is acceptable

The best examples of this are cases where it’s a true one-time-only action. For instance:

  • Logging that a component was viewed

Step 4.5: componentWillReceiveProps, componentDidReceiveProps etc

As above, these collapse into useEffects.

Step 5: componentWillUnmount

Generally speaking, in unmount we have cleanup code. For instance, we’ll clean up old intervals, kill any connections, etc, etc. Again, as with the theme of the last few steps of this document, is that we want to think of these as “clean up per-variable”, not “clean up per-component”.

The return value of the function passed into useEffect is executed when the component un-mounts. You can use this function to clean up any state related to this useEffect. Here’s an example:

Notice how nice it is that hooks allow us to consolidate all build-up and tear-down code in the same location.

Step 6: renderXXX methods

By the way — does your component have renderXYZ methods — for instance, does it have methods other than the render method which return JSX? These are actually a bit of an anti-pattern! Not to worry — just convert these methods to new components.

Step 7: Class Methods

If you’ve been following this guide up until this point, you’ll have a fairly functional (pun definitely intended) React component! The last things remaining are whatever class methods are left over. Some of these will be event methods, and the rest will just be helper functions. These are simple to convert: they just become nested functions inside the function component.

You have two options to declare these functions: const-declared functions (such as const handleClick = () => … and, er, function-declared functions (such as function handleClick() { …. The advantage of const functions is that you can wrap them with useCallback, which can prevent confusion down the line. The advantage of function functions is that you don’t have to worry if an early function is referring to a later one, as they are all hoisted. Not sure which one to pick? Choose const.

If you’re using const functions, we recommend useCallback. Every time your function is executed, if you don’t use useCallback, you’ll be creating new functions. These can lead to unnecessary re-renders. They can also lead to confusion about why a component is re-rendering even when its props appear unchanged.

👀Sidebar! 👀

Soon, React will provide useEvent for event handlers. It’s the same as useCallback, except it doesn’t take a dependency array, meaning it’s even simpler to use. Watch this space!

Here’s a link to the RFC for the curious.

Step 8:

You did it!!! You’re done! You made the world a better place.

Curious to see what else the team is up to? Check out our more of our engineering blog or hop over to the Gem careers page to learn more about #LifeAtGem.

--

--