DEV Community

Cover image for Streamlining Constructors in Functional React Components
Adam Nathaniel Davis
Adam Nathaniel Davis

Posted on • Edited on

Streamlining Constructors in Functional React Components

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.

Top comments (23)

Collapse
 
miketalbot profile image
Mike Talbot ⭐

I always use:

useMemo(()=>doThisOnceRightNow(), [])

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Oooh, I like this.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

BTW, I even added this at the end of the article as the best solution (and credited you).

Collapse
 
ecyrbe profile image
ecyrbe

But this is wrong.

useMemo is a cache layer, no guaranty your useMemo will not be trigerred twice if the cache get evicted (which it does if you use suspended components or react 18 concurrent rendering).

Check useMemo docs about useMemo and suspend :
beta.reactjs.org/reference/react/u...
And also this issue that confirm this behaviour: github.com/facebook/react/issues/1...

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

OK, correct me if I'm wrong (and I very well might be), but it seems that both of the links you provide ultimately resolve to the issue of these items being called twice in development mode???

Thread Thread
 
ecyrbe profile image
ecyrbe

No, read carrefully :

Both in development and in production, React will throw away the cache if your component suspends during the initial mount

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis • Edited

OK, but...

(And I'm not trying to be resistant here - I'm honestly interested in understanding this process as well as I can.) What exactly makes makes the component "suspend" during the initial mount? And if it did suspend, then doesn't it make logical sense that you would want that function to be called again when it re-mounts??

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis • Edited

Specifically, it says, "This should match your expectations if you rely on useMemo solely as a performance optimization."

But this is (sorta) a "performance optimization". Yes, there's a procedural aspect to it as well (as in, I want this logic to be called before the render), but there's also a performance aspect (as in, I only want this logic to be called once for the entire lifecycle of the component).

Thread Thread
 
ecyrbe profile image
ecyrbe • Edited

What exactly makes makes the component "suspend" during the initial mount?

using suspense + throw a promise.

And if it did suspend, then doesn't it make logical sense that you would want that function to be called again when it re-mounts?

Maybe yes. Depends on what you wanted to do on construction. but if no side effects (no api call, no logging, etc) you're of course good to execute it twice.

So still be carreful, because they say this :

React may add more features that take advantage of throwing away the cache—for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport

This means, cache can be evicted in the future without even unmounting.

I wanted to higlight a potential unexpected behaviour like the one reported on the issue.
So me saying "this is wrong" was a bit too harsh. A better answer would have been: "be aware of the caveats, it's not bullet proof".

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

Ohhhh, I totally get it. And I appreciate it. I've been doing React dev now for 6 years and sometimes I get to feeling like I know it all. And then... I'm slapped back into reality.

So I definitely appreciate the clarifications. Yeah... "this is wrong" may have been an overly-strong retort. But it's good as an impetus to do a "deeper dive".

Thanks!

 
miketalbot profile image
Mike Talbot ⭐

Agreed, given that throwing an exception will unwind the state I expected that all state stored things go away (as your link seems to confirm), given that the useState initializer and the useMemo get re-run I'm presuming that the useRefs also get a new state - do you know if they do? Effectively Suspense is catching an expectionhigher up - to my understanding, the component is obliterated and is going to be initialised again in that case.

Suspense boundary within the component or an awareness that suspending causes remounting are the ways perhaps?

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

That's basically the React Hooks version of an IIFE singleton.

Collapse
 
ecyrbe profile image
ecyrbe • Edited

Hello adam,

Nice tip, i would add some more :

  • if you need to compute something on pre-render, check first if useLayoutEffect don't answer your need first.
  • if useLayoutEffect don't work and what you compute is to be put on a state, use useState initial function alternative :
const [state, setState] = useState(()=> {
  return computesomething;
});
Enter fullscreen mode Exit fullscreen mode
  • if you resort to use this pattern, encapsulate it on a custom hook like this to convey meaning
function useOnce(callback){
  const once = useRef(false);

  if (!once.current) {
    once.current = true;
    callback();
  }
}
Enter fullscreen mode Exit fullscreen mode

and use it like this :

export const App = () => {
  const [counter, setState] = useState(0);
  useOnce(() => {
    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
Collapse
 
bytebodger profile image
Adam Nathaniel Davis

This is essentially what I did in my previous approach with useConstructor(). Granted, in my original approach, I used a state variable as the tracking variable. But I've since updated it to use a ref.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

Also, although I like useLayoutEffect(), I'm resistant to use it much because it delays display features.

Collapse
 
aminnairi profile image
Amin • Edited

Hi and thank you for your article!

What is the main benefit of using this technique over using a useEffect?

I believe you could have a code that runs when the component gets initialized using this piece of code.

useEffect(() => {
  // Constructor-like instructions here
}, []);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

useEffect() is the out-of-the-box "answer" given in the React documentation for how to handle constructor-like functionality in a functional component. For the most part... they're right. But it depends on how specific you feel about the need for true "constructor-like" functionality.

In the example you've given, that code will indeed run once, and only once, for the entire lifecycle of the component. This is ensured by the fact that you've given it an empty dependency array.

However, effects always run after the rendering cycle. Granted, in most cases, it's probably sufficient to simply use useEffect() and allow it to do it's processing after the rendering cycle. But in the opening comments to this article (and in its predecessor), I stated that I want my "constructor" to run:

before anything else in the life-cycle of this component

If you truly want that block of code to run before the rendering cycle, useEffect() will not help you.

Collapse
 
aminnairi profile image
Amin • Edited

Thanks for your reply!

Do you have any real-world use case scenarios that may help us understand where a useEffect may be less interesting to use than the solution you provided?

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

Of course, it's gonna be on a case-by-case basis. But where I originally ran into this was when I was trying to launch API calls - calls that I knew may be a bit "laggy", so I wanted them to be launched before the render cycle.

Another use-case that I've run into is where you need to do pre-processing of variables that go beyond simply setting the initial value of a state variable. In old-skool React (meaning: class-based React components), the most common use of the constructor was to set the initial value of state variables. And in React's documentation, they talk confidently about how you don't need a constructor anymore because you can use const [someVar, setSomeVar] = useState('theInitialValueOfSomeVar');.

But what if you're trying to initialize variables that aren't state variables? What if you want to ensure that some "tracking" variables are set to the proper state before the component even starts doing it's work?

Again... I'll stress that this is actually an "edge case". For the vast majority of components you create, you'll never need a "true" constructor-like functionality. But it's a bit silly to assume that there's never any more need for a constructor just because you can now use useState('initial value').

Collapse
 
sureisfun profile image
Barry Melton

@amin had the same question I had, but since your answer was as thorough as it was, I didn't need to ask it (for which I owe you thanks!)

Collapse
 
ngist profile image
n-gist • Edited

Perhaps useState itself fits better. It inherits the functionality of data initialization inside constructors of the component classes, is executed once, and does not depend on the parameters or decisions of React to flush the cache.
The base use would be:

useState(() => {
    // one-time
})
Enter fullscreen mode Exit fullscreen mode

If need to get initialized data, return it as object

const [ constructed ] = useState(() => {
    const rendersCount = 0
    return { rendersCount }
})

constructed.rendersCount++
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Ahhh, that's another interesting approach. I hadn't even considered using a function call in the constructor of useState(). Thanks!

Collapse
 
darkterminal profile image
Imam Ali Mustofa • Edited

I am used this method everytime but i can't explain in detail like you. That's 🔥