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

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 🔥