DEV Community

Cover image for Streamlining Constructors in Functional React Components

Streamlining Constructors in Functional React Components

Adam Nathaniel Davis on February 08, 2023

Several years ago, I wrote an article about how to create constructor-like functionality in React with function-based components. (You can read it...
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 🔥