Streamlining Constructors in Functional React Components

Adam Nathaniel Davis - Feb 8 '23 - - Dev Community

Several years ago, I wrote an article about how to create constructor-like functionality in React with function-based components. (You can read it here: https://dev.to/bytebodger/constructors-in-functional-components-with-hooks-280m) I won't repeat the entire contents of that article, but the basic premise was this:

  1. Class-based React components give you the ability to use a constructor.

  2. Function-based React components have no direct equivalent to the constructor. (Because functions, by their very nature, don't have constructors.)

  3. The core React documentation implies that you simply don't need constructors anymore. And to be fair, usually you don't need them. But it's myopic to imply that no developer, writing function-based components, will EVER need to use constructor-like functionality.

Also to be clear, this is what I'm defining as a "constructor":

  1. Code that runs before anything else in the life-cycle of this component.

  2. Code that runs once, and only once, for the entire life-cycle of this component.

In that article, I demonstrated a technique to "spoof" constructor-like functionality in function-based components. In fact, I even created an NPM package for my useConstructor() feature.

Some of you may scoff at this idea, owing to React's official stance that constructors simply aren't needed - at all - in modern React applications. However, a funny thing happened after I published that article.

That article became my second-most-read post. To-date, it's received almost 90,000 views. In case it's not clear, this is a strong indication that:

  1. Many people still wonder how to implement constructor-like functionality in function-based React components (regardless of the fact that the core documentation claims that you don't need them at all).

  2. And apparently, there are many people out there Googling on this subject. Because most of the views that I've received on that article have come, in a steady stream, over many months (and... years) since I wrote the original article.

So why am I writing on the subject again???

Well, it turns out that my first solution was not "bad". But it was also, umm... suboptimal. Overly complicated. A bit convoluted, if you will. So I'm going to show you a simpler way to create "constructors" in function-based React components, without leveraging state values or NPM packages.


Image description

The Sample Application

(NOTE: If you want to see a live example of all the subsequent code, you can check it out here: https://stackblitz.com/edit/constructor-functionality-in-react-functional-components)

Our dead-simple example will just have one component that looks like this:

export const App = () => {
  const [counter, setState] = useState(0);

  const increment = () => setState(counter + 1);

  return <>
    <div>
      Counter: {counter}
    </div>
    <div>
      <button onClick={increment}>
        Increment
      </button>
    </div>
    <Child/>
  </>;
};
Enter fullscreen mode Exit fullscreen mode

This is just your standard demo/counter app. It renders a state value for the current value of counter. And it gives you a button that you can click to increment that value.

So let's imagine that you have some bit of "pre-processing" that you want to happen the first time that <App/> is invoked. Remember, in keeping with our guidelines for what a constructor should do, we want this pre-processing to happen before anything else in the component. (Essentially, we want the pre-processing to occur before the initial render.) We also want the pre-processing to run once, and only once, for the entire lifecycle of the component. This is where useRef() will come in handy.


Image description

Potential Solutions

A ref is a value that remains in memory between renders. It's not the same as a state variable - because updating state variables triggers the reconciliation process. In other words, you use state variables when you have values that will influence the display. But you use refs when you have values that should stay in memory - but are not used in the display.

Utilizing useRef(), our proposed constructor would look like this:

export const App = () => {
  const [counter, setState] = useState(0);
  const constructorHasRun = useRef(false);

  const constructor = () => {
    if (constructorHasRun.current !== false) 
      return;
    constructorHasRun.current = true;
    // put your constructor code HERE
    console.log('constructor invoked at ', window.performance.now());
  };

  constructor();

  const increment = () => setState(counter + 1);

  return <>
    <div>
      Counter: {counter}
    </div>
    <div>
      <button onClick={increment}>
        Increment
      </button>
    </div>
  </>;
};
Enter fullscreen mode Exit fullscreen mode

When the app loads, you can see in the console that the constructor only runs once. No matter how many times you click the "Increment" button thereafter, the constructor logic is never triggered again.

[NOTE: If you are in strict mode, React will call this function twice. That can lead to some confusion while you're developing. This is expected behavior that React uses to "help you find accidental impurities". This is development-only behavior and does not affect production.]

The constructorHasRun ref value serves as a tracking variable. Once it's set from false to true, the constructor code will never run again for the lifecycle of the app.

This approach works just fine - but it still feels a little clunky to me. Under this approach, you must first define the constructor function, and then remember to invoke it somewhere before the return statement. Ideally, you'd probably like your constructor code to just... run. Luckily, we can achieve this outcome by using an Immediately Invoked Function Expression (IIFE). That would look like this:

export const App = () => {
  const [counter, setState] = useState(0);
  const constructorHasRun = useRef(false);

  (() => {
    if (constructorHasRun.current !== false) 
      return;
    constructorHasRun.current = true;
    // put your constructor code HERE
    console.log('constructor invoked at ', window.performance.now());
  })();

  const increment = () => setState(counter + 1);

  return <>
    <div>
      Counter: {counter}
    </div>
    <div>
      <button onClick={increment}>
        Increment
      </button>
    </div>
  </>;
};
Enter fullscreen mode Exit fullscreen mode



Image description

The Best Solution

I added this section after originally publishing this article because @miketalbot recommended this in the comments. And honestly, it's cleaner, simpler, and just more elegant than using a tracking variable with useRef(). Here's his recommendation:

export const App = () => {
  const [counter, setState] = useState(0);

  useMemo(() => {
    // put your constructor code HERE
    console.log('constructor invoked at ', window.performance.now());
  }, []);

  const increment = () => setState(counter + 1);

  return <>
    <div>
      Counter: {counter}
    </div>
    <div>
      <button onClick={increment}>
        Increment
      </button>
    </div>
  </>;
};
Enter fullscreen mode Exit fullscreen mode

This works so well because useMemo() is used to cache the result of a computation. That means that, in order to first create the cache, useMemo() will first run the code. Then, because the dependency array is empty, the cached computation will never be re-run again.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player