Skip to content

Transitioning from React class components to function components with hooks

Transitioning from React class components to function components with hooks

It has been approximately one year since React v16.8 was released, marking the introduction of Hooks. Yet, there are still people used to React class components who still haven't experienced the full potential of this new feature, along with functional components, including myself. The aim of this article is to summarize and encompass the most distinguishable features of the class components, and respectively show their alternatives when using React hooks.

Functional components

Before we start with Hooks examples, we will shortly discuss functional components in case you aren't familiar. They provide an easy way to create new units without the need of creating a new class and extending React.Component.

Note: Keep in mind that functional components have been part of React since it's creation.

Here is a very simple example of a functional component:

const Element = () => (
  <div className="element">
    My Element
  </div>
);

And just like class components, we can access the properties. They are provided as the first argument of the function.

const Element = ({ text }) => (
  <div className="element">
    {text}
  </div>
);

However, these type of components--while very convenient for simple UI elements--used to be very limited in terms of life cycle control, and usage of state. This is the main reason they had been neglected until React v16.8.

Component state

Let's take a look at the familiar way of how we add state to our object-oriented components. The example will represent a component which renders a space scene with stars; they have the same color. We are going to use few utility functions for both functional and class components.

  • createStars(width: number): Star[] - Creates an array with the star objects that are ready for rendering. The number of stars depends on the window width.
  • renderStars(stars: Star[], color: string): JSX.Element - Builds and returns the actual stars markup.
  • logColorChange(color: string) - Logs when the color of the space has been changed.

and some less important like calculateDistancesAmongStars(stars: Star[]): Object.

We won't implement these. Consider them as black boxes. The names should be sufficient enough to understand their purpose.

Note: You may find a lot of demonstrated things unnecesary. The main reason I included this is to showcase the hooks in a single component.

And the example:

Class components

class Space extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      stars: createStars(window.innerWidth)
    };
  }

  render() {
    return (
      <div className="space">
        {renderStars(this.state.stars, this.props.color)}
      </div>
    );
  }
}

Functional components

The same can be achieved with the help of the first React Hook that we are going to introduce--useState. The usage is as follows: const [name, setName] = useState(INITIAL_VALUE). As you can see, it uses array destructuring in order to provide the value and the set function:

const Space = ({ color }) => {
  const [stars, setStars] = useState(createStars(window.innerWidth));

  return (
    <div className="space">
      {renderStars(stars, color)}
    </div>
  );
};

The usage of the property is trivial, while setStars(stars) will be equivalent to this.setState({ stars }).

Component initialization

Another prominent limitation of functional components was the inability to hook to lifecycle events. Unlike class components, where you could simply define the componentDidMount method, if you want to execute code on component creation, you can't hook to lifecycle events. Let's extend our demo by adding a resize listener to window which will change the number of rendered stars in our space when the user changes the width of the browser:

Class components

class Space extends React.Component {
  constructor(props) { ... }

  componentDidMount() {
    window.addEventListener('resize', () => {
      const stars = createStars(window.innerWidth, this.props.color);
      this.setState({ stars });
    });
  }

  render() { ... }
}

Functional components

You may say: "We can attach the listener right above the return statement", and you will be partially correct. However, think about the functional component as the render method of a class component. Would you attach the event listener there? No. Just like render, the function of a functional component could be executed multiple times throughout the the lifecycle of the instance. This is why we are going to use the useEffect hook.

It is a bit different from componentDidMount though--it incorporates componentDidUpdate, and componentDidUnmount as well. In other words, the provided callback to useEffect is executed on every update. Anyhow, you can have certain control with the second argument of useState - it represents an array with the values/dependencies which are monitored for change. If they do, the hook is executed. In case the array is empty, the hook will be executed only once, during initialization, since after that there won't be any values to be observed for change.

const Space = ({ color }) => {
  const [stars, setStars] = useState(createStars(window.innerWidth));

  useEffect(() => {
    window.addEventListener('resize', () => {
      const stars = createStars(window.innerWidth, color);
      setStars(stars);
    });
  }, []); // <-- Note the empty array

  return (
    ...
  );
};

Component destruction

We added an event listener to window, so we will have to remove it on component unmount in order to save us from memory leaks. Respectively, that'll require keeping a reference to the callback:

Class components

class Space extends React.Component {
  constructor(props) { ... }

  componentDidMount() {
    window.addEventListener('resize', this.__resizeListenerCb = () => {
      const stars = createStars(window.innerWidth, this.props.color);
      this.setState({ stars });
    });
  }

  componentDidUnmount() {
    window.removeEventListener('resize', this.__resizeListenerCb);
  }

  render() { ... }
}

Functional component

For the equivalent version of the class component, the useEffect hook will execute the returned function from the provided callback when the component is about to be destroyed. Here's the code:

const Space = ({ color }) => {
  const [stars, setStars] = useState(createStars(window.innerWidth));

  useEffect(() => {
    let resizeListenerCb;

    window.addEventListener('resize', resizeListenerCb = () => {
      const stars = createStars(window.innerWidth, color);
      setStars(stars);
    });

    return () => window.removeEventListener('resize', resizeListenerCb);
  }, []); // <-- Note the empty array

  return (
    ...
  );
};

An important remark

It is worth mentioning that, when you work with event listeners or any other methods that defer the execution in the future of a callback/function, you should take into account that the state provided to them is not mutable.

Taking the window listener we use in our demo as an example; if we used thestars state inside the callback, we would get the exact value at the moment of the definition (callback), which means that, when the callback is executed, we risk having a stale state.

There are various ways to handle that, one of which is to re-register the listener every time the stars are changed, by providing the stars value to the observed dependency array of useEffect.

Changed properties

We already went through useEffect in the sections above. Now, we will briefly show an example of componentDidUpdate. Let's say that we want to log the occurances of color change to the console:

Class components

class Space extends React.Component {
  ...

  componentDidUpdate(prevProps) {
    if (this.props.color !== prevProps.color) {
      logColorChange(this.props.color);
    }
  }

  ...
}

Functional components

We'll introduce another useEffect hook:

const Space = ({ color }) => {
  ...

  useEffect(() => {
    logColorChange(color);
  }, [color]); // <-- Note that this time we add `color` as observed dependency

  ...
};

Simple as that!

Changed properties and memoization

Just as an addition to the example above, we will quickly showcase useMemo; it provides an easy way to optimize your component when you have to perform a heavy calculation only when certain dependencies change:

const result = useMemo(() => expensiveCalculation(), [color]);

References

Due to the nature of functional components, it becomes hard to keep a reference to an object between renders. With class components, we can simply save one with a class property, like:

class Space extends React.Component {
  ...

  methodThatIsCalledOnceInALifetime() {
    this.__distRef = calculateDistancesAmongStars(this.state.stars);
  }

  ...
}

However, here is an example with a functional component that might look correct but it isn't:

const Space = ({ color }) => {
  ...

  let distRef; // Declared on every render.

  function thatIsCalledOnceInALifetime() {
    distRef = caclulateDistancesAmongStars(stars);
  }

  ...
};

As you can see, we won't be able to preserve the output object with a simple variable. In order to do that, we will take a look at yet another hook named useRef, which will solve our problem:

const Space = ({ color }) => {
  ...
  const distRef = useRef();

  function thatIsCalledOnceInALifetime() {
    // `current` keeps the same reference
    // throughout the lifetime of the component instance
    distRef.current = caclulateDistancesAmongStars(stars);
  }

  ...
}

The same hook is used when we want to keep a reference to a DOM element.

Conclusion

Hopefully this should give you a starting point when it comes to using React Hooks for the things that you are already used to doing with class components. Obviously, there are more hooks to explore, including the definition of custom ones. For all of that, you can head to the official docs. Give them a try and experience the potential of functional React!

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

State Management with React: Client State and Beyond cover image

State Management with React: Client State and Beyond

Introduction State management has long been a hotly debated subject in the React world. Best practices have continued to evolve, and there’s still a lot of confusion around the subject. In this article, we are going to dive into what tools we might want to use for solving common problems we face in modern web application development. There are different types of state and different types of ways to store and manage your application state. For example, you might have some local client state in a component that controls a dropdown, and you might also have a global store that contains authenticated user state. Aside from those typical examples, on some pages of your website, you might store some state in the URL. For quite a while, we’ve been leaning into global client state in single-page app land. This is what state management tools like Redux and other similar libraries help us out with. In this post, we will cover these different strategies in more detail so that we can try to make the best decisions going forward regarding state management in our applications. Local and global client state In this section, we'll discuss the differences between local and global client state, when to use each, and some popular libraries that can help you manage them effectively. When to use local client state Local client state is best suited for managing state that is specific to a single component or a small group of related components. Examples of local state include form inputs, component visibility, and UI element states like button toggles. Using local state keeps the component self-contained, making it easier to understand and maintain. In general, it's a good idea to start with local state and only use global state when you have a clear need for it. This can help keep your code simple and easy to understand. When to use global client state Global client state is useful when you have state that needs to be accessed by multiple unrelated components, or when the state is complex and would benefit from a more carefully designed API. Common examples of global state include user authentication, theme preferences, and application-wide settings. By centralizing this state, you can easily share it across the entire application, making it more efficient and consistent. If you’re building out a feature that has some very complex state requirements, this could also be a good case for using a “global” state management library. Truth is, you can still use one of these libraries to manage state that is actually localized to your feature. Most of these libraries support creating multiple stores. For example, if I was building a video chat client like Google Meets with a lot of different state values that are constantly changing, it might be a good idea to create a store to manage the state for a video call. Most state management libraries support more features than what you get out of the box with React, which can help design a clean and easy to reason about modules and APIs for scenarios where the state is complex. Popular libraries for global state management There are a lot of great libraries for managing state in React out there these days. I think deciding which might be best for you or your project is mostly a matter of preference. Redux and MobX are a couple of options that have been around for quite a long time. Redux has gained a reputation for being overly complex and requiring a lot of boilerplate code. The experience is actually much improved these days thanks to the Redux Toolkit library. MobX provides an easy-to-use API that revolves around reactive data/variables. These are both mature and battle-tested options that are always worth considering. Meta also has a state management named Recoil that provides a pretty easy-to-use API that revolves around the concept of atoms and selectors. I don’t see this library being used a ton in the wild, but I think it’s worth mentioning. A couple of the more popular new players on the block are named jotai and zustand. I think after the Redux hangover, these libraries showed up as a refreshing oasis of simplicity. Both of these libraries have grown a ton in popularity due to their small byte footprints and simple, straightforward APIs. Context is not evil The React Context API, like Redux, has also been stigmatized over the years to the point where many developers have their pitchforks out, declaring that you should never use it. We leaned on it for state management a bit too much for a while, and now it is a forbidden fruit. I really dislike these hard all-or-nothing stances though. We just need to be a little bit more considerate about when and where we choose to use it. Typically, React Context is best for storing, and making available global state that doesn’t change much. Some of the most common use cases are things like themes, authentication, localization, and user preferences. Contrary to popular belief, only components (and their children) that use that context (const context = useContext(someContext);) are re-rendered in the event of a state change, not all of the children below the context provider. Storing state in the URL The most underused and underrated tool in the web app state management tool belt is using the URL to store state. Storing state in the URL can be beneficial for several reasons, such as enabling users to bookmark and share application state, improving SEO, and simplifying navigation. The classic example for this is filters on an e-commerce website. A good user experience would be that the user can select some filters to show only the products that they are looking for and then be able to share that URL with a friend and them see the same exact results. Before you add some state to a page, I think it’s always worth considering the question: “Should I be able to set this state from the URL?”. Tools for managing URL state We typically have a couple of different tools available to use for managing URL state. Built-in browser APIs like the URL class and URLSearchParams. Both of these APIs allow you to easily parse out parts of a URL. Most often, you will store URL state in the parameters. In most React applications, you will typically have a routing library available to help with URL and route state management as well. React Router has multiple hooks and other APIs for managing URL state like useLocation that returns a parsed object of the current URL state. Keeping URL and application state in sync The tricky part of storing state in the URL is when you need to keep local application state in sync with the URL values. Let’s look at an example component with a simple name component that stores a piece of state called name. ` import React, { useState, useEffect } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; function MyComponent() { const location = useLocation(); const history = useHistory(); const [name, setName] = useState(''); useEffect(() => { // Update the name state when the URL changes const searchParams = new URLSearchParams(location.search); setName(searchParams.get('name') || ''); }, [location]); function handleNameChange(event) { setName(event.target.value); // Update the URL when the name changes history.push(/my-component?name=${event.target.value}`); } return ( ); } ` The general idea is to pull the initial value off of the URL when the component mounts and set the state value. After that, in our event handler, we make sure to update the URL state as well as our local React state. Moving state to the server Moving web application state to the server can be beneficial in several scenarios. For example, when you have a complex state that is difficult to manage on the client-side. By moving the state to the server, you can simplify the client-side code and reduce the amount of data that needs to be transferred between the client and server. This can be useful for applications that have a lot of business logic or complex data structures. In most cases if there’s some logic or other work that you can move off of your client web application and onto the server that is a win. Conclusion State management is a crucial aspect of building modern web applications with React. By understanding the different types of state and the tools available for managing them, you can make informed decisions about the best approach for your specific use case. Remember to consider local and global client state, URL-based state, and server-side state when designing your application's state management strategy....

State of React Ecosystem Wrap-Up January 2023 cover image

State of React Ecosystem Wrap-Up January 2023

In this State of React Ecosystem event, our panelists discussed the React Roadmap, Best Practices, Redux, Styling Libraries and techniques, Nextjs, and community initiatives! In this wrap-up, we will take a deeper look into these latest developments and explore what is on the horizon for React. You can watch the full State of React Ecosystem event on the This Dot Media YouTube Channel. Here is a complete list of the host and panelists that participated in this online event. Hosts__ - Tracy Lee, CEO, This Dot Labs, @ladyleet - Adam L. Barrett, Senior Software Engineer, This Dot, @adamlbarrett Panelists__ - Mark Erikson, Senior Front-End Engineer, Replay, Redux maintainer @acemarke - Tanner Linsley, VP of UI/UX, nozzle.io, TanStack Creator, @tannerlinsley - Dominik Dorfmeister, TanStack Query Maintainer, @TkDodo - Ben Ilegbodu, Frontend Architect, Stitch Fix, @benmvp - Chantastic, DX Engineer, Chromatic, @chantastic - Kathleen McMahon, Senior Design Systems Engineer, Northwestern Mutual, @resource11 Updates from Panelists and what they are currently working on The event started off with great conversation by getting updates, from our panelists, on what they are currently working on. Mark Erikson__ was first, and he talked about how Redux Toolkit 1.9 was shipped back in November 2022. It had new options for RTK Query as well as forms improvements by optimizing the internals. Redux Toolkit 2.0 is currently being developed! It will have breaking changes, and there are some big things coming with the release including modernizing the build output, and updating the package to be fully es module format package defined, and much more! Redux Toolkit 2.0 alpha 1 has already been shipped, and he is asking for users to try it out, and give feedback. Kathleen McMahon__ was next, and talked about how she has been with Nortwestern Mutual and is currently working on standing up their design tokens system as part of the Lunar Design system. She has been enjoying the work of trying to get things into a very big Enterprise level system. She also got the opportunity to give a couple talks last fall at React Brussels and React Verona where she talked about feature flagging, accessibility, and how to make some widgets accessible. She will be giving a similar talk at React Miami. Tanner Linsley__ gave a quick update that he is currently working on Type Safe Routing and has been for over a year learning about the intricacies of how all of that works with type safe everything. He mentioned that there is also going to be a lot of new TanStack packages to play around with. He has also been recently trying to rebuild all the reactivity engine using SolidJS, and he is working on some smaller libraries at TanStack that are going to be headless. Dominik Dorfmeister__ gave an update about what is coming for TanStack Query. In a recent update, they added a Svelte query adapter for TanStack Query. They have pretty much completed the cycle of having one adapter for all the major frameworks. He takes time to share a video that shows Astro with the same query client shared with four different components with each one in a different framework. It shows what you can do when everything works together with the same query client. He also goes over the roadmap for getting to TanStack Query v5 with it hopefully coming out sometime this year. Ben Ilegbodu__ is a Frontend Architect at Stitch Fix on the frontend platform team. He is part of a team that builds out all the platforms and design systems that other teams use to build a site. He has been helping build out the design system using React while documenting in Storybook. He also talks about how the team has been building a development platform for all the teams to build their apps. They are in the process of moving from Rails to NextJS using a wrapper. He finishes up by talking about working on a monorepo for all of their libraries. Chantastic__ talks about how he has been trying to do a little more with Youtube this year. He works at Chromatic which is a company that maintains StorybookJS. They are all working to get to Storybook day which is March 14, 2023. It’s an event that is going to be going over everything that is happening with Storybook 7. This concluded introduction as the event moved into general questions for the panel. Panel Questions Adam got the conversation going to this next part of the event by asking right if React is dying? The panelists got on this topic and maintained it throughout the rest of the event. Each panelist took the time to explain how it is either frustrating or misunderstood. There is still growth and evolution within the React ecosystem. With new frameworks and libraries comes a betterment for the community as a whole. They also brought up how the same sorts of things were said about other frameworks like Angular. Overall, there was agreement that React is not dead, but in a time where it is growing to be a better product overall. Conclusion The one question that was asked helped bring out an incredible conversation on where React is and where it is going. It is always good to continue to have these discussions in order to stay current or talk about what needs to be done to keep things moving forward for the future of the library. You can watch the full State of React Ecosystem event on the This Dot Media Youtube Channel....

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights) cover image

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights)

We take accessibility quite seriously here at This Dot because we know it's important. Still, throughout my career, I've seen many projects where accessibility was brushed aside for reasons like "our users don't really use keyboard shortcuts" or "we need to ship fast; we can add accessibility later." The truth is, that "later" often means "never." And it turns out, anyone could break their hand, like I did. I broke my dominant hand and spent four weeks in a cast, effectively rendering it useless and forcing me to work left-handed. I must thus apologize for the misleading title; this post should more accurately be dubbed "second-hand" accessibility insights. The Perspective of a Developer Firstly, it's not the end of the world. I adapted quickly to my temporary disability, which was, for the most part, a minor inconvenience. I had to type with one hand, obviously slower than my usual pace, but isn't a significant part of a software engineer's work focused on thinking? Here's what I did and learned: - I moved my mouse to the left and started using it with my left hand. I adapted quickly, but the experience wasn't as smooth as using my right hand. I could perform most tasks, but I needed to be more careful and precise. - Many actions require holding a key while pressing a mouse button (e.g., visiting links from the IDE), which is hard to do with one hand. - This led me to explore trackpad options. Apart from the Apple Magic Trackpad, choices were limited. As a Windows user (I know, sorry), that wasn't an option for me. I settled for a cheap trackpad from Amazon. A lot of tasks became easier; however, the trackpad eventually malfunctioned, sending me back to the mouse. - I don't know a lot of IDE shortcuts. I realized how much I've been relying on a mouse for my work, subconsciously refusing to learn new keyboard shortcuts (I'll be returning my senior engineer license shortly). So I learned a few new ones, which is good, I guess. - Some keyboard shortcuts are hard to press with one hand. If you find yourself in a similar situation, you may need to remap some of them. - Copilot became my best friend, saving me from a lot of slow typing, although I did have to correct and rewrite many of its suggestions. The Perspective of a User As a developer, I was able to get by and figure things out to be able to work effectively. As a user, however, I got to experience the other side of the coin and really feel the accessibility (or lack thereof) on the web. Here are a few insights I gained: - A lot of websites apparently tried_ to implement keyboard navigation, but failed miserably. For example, a big e-commerce website I tried to use to shop for the aforementioned trackpad seemed to work fine with keyboard navigation at first, but once I focused on the search field, I found myself unable to tab out from it. When you make the effort to implement keyboard navigation, please make sure it works properly and it doesn't get broken with new changes. I wholeheartedly recommend having e2e tests (e.g. with Playwright) that verify the keyboard navigation works as expected. - A few websites and web apps I tried to use were completely unusable with the keyboard and were designed to be used with a mouse only. - Some sites had elaborate keyboard navigation, with custom keyboard shortcuts for different functionality. That took some time to figure out, and I reckon it's not as intuitive as the designers thought it would be. Once a user learns the shortcuts, however, it could make their life easier, I suppose. - A lot of interactive elements are much smaller than they should be, making it hard to accurately click on them with your weaker hand. Designers, I beg you, please make your buttons bigger. I once worked on an application that had a "gloves mode" for environments where the operators would be using gloves, and I feel like maybe the size we went with for the "gloves mode" should be the standard everywhere, especially as screens get bigger and bigger. - Misclicking is easy, especially using your weaker hand. Be it a mouse click or just hitting an Enter key on accident. Kudos to all the developers who thought about this and implemented a confirmation dialog or other safety measures to prevent users from accidentally deleting or posting something. I've however encountered a few apps that didn't have any of these, and those made me a bit anxious, to be honest. If this is something you haven't thought about when developing an app, please start doing so, you might save someone a lot of trouble. Some Second-Hand Insights I was only a little bit impaired by being temporarily one-handed and it was honestly a big pain. In this post, I've focused on my anecdotal experience as a developer and a user, covering mostly keyboard navigation and mouse usage. I can only imagine how frustrating it must be for visually impaired users, or users with other disabilities, to use the web. I must confess I haven't always been treating accessibility as a priority, but I've certainly learned my lesson. I will try to make sure all the apps I work on are accessible and inclusive, and I will try to test not only the keyboard navigation, ARIA attributes, and other accessibility features, but also the overall experience of using the app with a screen reader. I hope this post will at least plant a little seed in your head that makes you think about what it feels like to be disabled and what would the experience of a disabled person be like using the app you're working on. Conclusion: The Humbling Realities of Accessibility The past few weeks have been an eye-opening journey for me into the world of accessibility, exposing its importance not just in theory but in palpable, daily experiences. My short-term impairment allowed me to peek into a life where simple tasks aren't so simple, and convenient shortcuts are a maze of complications. It has been a humbling experience, but also an illuminating one. As developers and designers, we often get caught in the rush to innovate and to ship, leaving behind essential elements that make technology inclusive and humane. While my temporary disability was an inconvenience, it's permanent for many others. A broken hand made me realize how broken our approach towards accessibility often is. The key takeaway here isn't just a list of accessibility tips; it's an earnest appeal to empathize with your end-users. "Designing for all" is not a checkbox to tick off before a product launch; it's an ongoing commitment to the understanding that everyone interacts with technology differently. When being empathetic and sincerely thinking about accessibility, you never know whose life you could be making easier. After all, disability isn't a special condition; it's a part of the human condition. And if you still think "Our users don't really use keyboard shortcuts" or "We can add accessibility later," remember that you're not just failing a compliance checklist, you're failing real people....