DEV Community

loading...
Cover image for Constructors in Functional Components With Hooks

Constructors in Functional Components With Hooks

bytebodger profile image Adam Nathaniel Davis Updated on ・8 min read

[NOTE: Since writing this article, I've encapsulated my little constructor-like utility into an NPM package. You can find it here: https://www.npmjs.com/package/@toolz/use-constructor]

When you're building functional components in React, there's a little feature from class-based components that simply has no out-of-the-box equivalent in functions. This feature is called a constructor.

In class-based components, we often see code that uses a constructor to initialize state, like this:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0
    };
  }

  render = () => {
    return (
      <button
        onClick={() =>
          this.setState(prevState => {
            return { counter: prevState.counter + 1 };
          })
        }
      >
        Increment: {this.state.counter}
      </button>
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

Honestly, I've come to view code like this as silly and unnecessarily verbose. Because even in the realm of class-based components, the exact same thing can be done like this:

class App extends Component {
  state = { counter: 0 };

  render = () => {
    return (
      <button
        onClick={() =>
          this.setState(prevState => {
            return { counter: prevState.counter + 1 };
          })
        }
      >
        Increment: {this.state.counter}
      </button>
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

As you see, there's no need for a constructor simply to initialize your state variables. You can declare initial state directly under the class.

Constructors... for Functions?

If we transition into the functional/Hooks side of things, it would seem that the Hooks team had the same idea. Because when you look at the FAQ for Hooks, it has a section dedicated to answering, "How do lifecycle methods correspond to Hooks?" The first bullet point in this section says:

constructor: Function components don’t need (emphasis: mine) a constructor. You can initialize the state in the useState call. If computing the initial state is expensive, you can pass a function to useState.

Wow...

I don't know if this "answer" is ignorant. Or arrogant. Or both. But it doesn't surprise me. It's similar to some of the other documentation I've seen around Hooks that makes all sorts of misguided assumptions for you.

This "answer" is ignorant because it assumes that the only reason for a constructor is to initialize state.

This "answer" is arrogant because, based on its faulty assumptions, it boldly states that you don't need a constructor. It's like going to the dentist for a toothache - but the dentist doesn't fix the problem. He just pats you on the head and says, "There, there. You don't really need that tooth. Now run along..."

The massive oversimplification in their dismissive FAQ overlooks the basic fact that there are other, perfectly-valid use-cases for a constructor (or, constructor-like functionality) that have nothing to do with initializing state variables. Specifically, when I think of a constructor, I think of these characteristics.

  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.

To be clear, is a constructor usually needed in most components? No. Certainly not. In fact, I'd say that the need for constructor-type logic is the exception, not the rule. Nevertheless, there are certain times when I absolutely need logic to run before anything else in the life-cycle of this component, and I absolutely need to ensure that it will run once, and only once, for the entire life-cycle of this component.

So despite the Hooks team's bold assertions, the fact is that there are times when I do need a constructor (or some equivalent).

The Challenge of Functional/Hooks Life-Cycles

The biggest "problem" with life-cycles in functions/Hooks is that... there are none. A function doesn't have a life-cycle. It just... runs. Whenever you call it. So from that perspective, it's understandable that there's no easy, out-of-the-box equivalent for a constructor in a functional component.

But despite the Holy Praise that JS fanboys heap upon the idea of functional programming, the simple fact is that a functional component doesn't really "run" like a true function. Sure, you may have that comforting function keyword at the top of your code (or, even better, the arrow syntax). But once you've created a functional component in React, you've handed over control of exactly how and when it gets called.

That's why I often find it incredibly useful to know that I can create some bit of logic that will run once, and only once, before any other processing takes place in the component. But when we're talking about React functional components, how exactly do we do that? Or, more to the point, where do we put that logic so it doesn't get called repeatedly on each render?

Tracing the "Life-Cycle" of Functions/Hooks

(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-hook)

This will best be illustrated with some examples. So let's first look at a dead-simple example of logic that runs in the body of a function:

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

  console.log("This happens on EVERY render.");
  return (
    <>
      <div>Counter: {counter}</div>
      <div style={{ marginTop: 20 }}>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

This is the simplest illustration of a function's "life-cycle". In a class-based component, we had the comfort (IMHO) of a render() function. And if a particular bit of logic should not run on every re-render, the process was pretty straight-forward: Just don't put that logic in the render() function.

But functional components offer no out-of-the-box equivalent. There is no render() function. There is only a return. The return (and all the rest of the code in the body of the function) gets called every single time this function is called.

I will freely raise my hand and admit that this threw me for a loop when I first started writing functional components. I would put some bit of logic above the return, and then I'd be surprised/annoyed when I realized it was running every single time the function was called.

In hindsight, there's nothing surprising about this at all. The return is not analogous to a render() function. To put it in different terms, the entire function is the equivalent of the render() function.

So let's look at some of the other Hooks that are available to us out-of-the-box. First, I spent time playing with useEffect(). This leads to the following example:

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

  useEffect(() => {
    console.log(
      "This only happens ONCE.  But it happens AFTER the initial render."
    );
  }, []);

  console.log("This happens on EVERY render.");
  return (
    <>
      <div>Counter: {counter}</div>
      <div style={{ marginTop: 20 }}>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

This gets us a little closer to our goal. Specifically, it satisfies my second condition for a constructor. It is run once, and only once, for the entire life-cycle of this component.

The problem is that it still runs after the component is rendered. This is completely consistent with the Hooks documentation, because there it states that:

By default, effects run after (emphasis: mine) every completed render.

I also played around with useLayoutEffect(), which leads to this example:

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

  useEffect(() => {
    console.log(
      "This only happens ONCE.  But it happens AFTER the initial render."
    );
  }, []);

  useLayoutEffect(() => {
    console.log(
      "This only happens ONCE.  But it still happens AFTER the initial render."
    );
  }, []);

  console.log("This happens on EVERY render.");
  return (
    <>
      <div>Counter: {counter}</div>
      <div style={{ marginTop: 20 }}>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

useLayoutEffect() gets us no closer to having a true "constructor". useLayoutEffect() fires before useEffect(), but it still fires after the render cycle. To be fair, this is still completely consistent with the Hooks documentation, because useLayoutEffect() is still... an effect. And effects always fire after rendering.

So if we want something that truly approximates the functionality of a constructor, we'll need to manually control the firing of that function. Thankfully, this is totally in our control, if we're willing to manually crank out the code that's needed to support it. That would look like this:

const App = () => {
  const [counter, setCounter] = useState(0);
  const [constructorHasRun, setConstructorHasRun] = useState(false);

  useEffect(() => {
    console.log(
      "This only happens ONCE.  But it happens AFTER the initial render."
    );
  }, []);

  useLayoutEffect(() => {
    console.log(
      "This only happens ONCE.  But it still happens AFTER the initial render."
    );
  }, []);

  const constructor = () => {
    if (constructorHasRun) return;
    console.log("Inline constructor()");
    setConstructorHasRun(true);
  };

  constructor();
  console.log("This happens on EVERY render.");
  return (
    <>
      <div>Counter: {counter}</div>
      <div style={{ marginTop: 20 }}>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

This gets us a lot closer to the stated goals. The manual constructor() function runs once, and only once, for the "life-cycle" of this function. It accomplishes this goal by leveraging a manual state variable - constructorHasRun - and refusing to re-run the constructor() functionality if that variable has been flipped to true.

This... "works". But it feels very... manual. If you require constructor-like features in your functional components, then under this approach, you'd have to manually add the tracking variable to the state of every component in which it's used. Then you'd need to ensure that your constructor() function is properly set up to only run its logic based on the value in that state variable.

Again, this "works". But it doesn't feel particularly satisfying. Hooks are supposed to make our life easier. If I have to manually code this functionality in every component where I need constructor-like features, then it makes me wonder why I'm using functions/Hooks in the first place.

Custom Hooks to the Rescue

This is where we can leverage a custom Hook to standardize this process. By exporting this into a custom Hook, we can get much closer to having a "true" constructor-like feature. That code looks like this:

const useConstructor(callBack = () => {}) {
  const [hasBeenCalled, setHasBeenCalled] = useState(false);
  if (hasBeenCalled) return;
  callBack();
  setHasBeenCalled(true);
}

const App = () => {
  useConstructor(() => {
    console.log(
      "This only happens ONCE and it happens BEFORE the initial render."
    );
  });
  const [counter, setCounter] = useState(0);
  const [constructorHasRun, setConstructorHasRun] = useState(false);

  useEffect(() => {
    console.log(
      "This only happens ONCE.  But it happens AFTER the initial render."
    );
  }, []);

  useLayoutEffect(() => {
    console.log(
      "This only happens ONCE.  But it still happens AFTER the initial render."
    );
  }, []);

  const constructor = () => {
    if (constructorHasRun) return;
    console.log("Inline constructor()");
    setConstructorHasRun(true);
  };

  constructor();
  console.log("This happens on EVERY render.");
  return (
    <>
      <div>Counter: {counter}</div>
      <div style={{ marginTop: 20 }}>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

If you want to see it without the failed attempts to use useEffect() and useLayoutEffect(), and without the manual implementation of constructor(), it looks like this:

const useConstructor(callBack = () => {}) {
  const [hasBeenCalled, setHasBeenCalled] = useState(false);
  if (hasBeenCalled) return;
  callBack();
  setHasBeenCalled(true);
}

const App = () => {
  useConstructor(() => {
    console.log(
      "This only happens ONCE and it happens BEFORE the initial render."
    );
  });
  const [counter, setCounter] = useState(0);

  return (
    <>
      <div>Counter: {counter}</div>
      <div style={{ marginTop: 20 }}>
        <button onClick={() => setCounter(counter + 1)}>Increment</button>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

By leveraging a custom Hook, we can now import the "constructor-like" functionality into any functional component where it's needed. This gets us, umm... 99% of the way there.

Why do I say that it's only 99% effective?? It satisfies both of my conditions for a "constructor". But... it only accomplishes this goal, in the example shown above, because I invoked it at the very top of the function.

There's still nothing stopping me from putting 100 lines of logic above the useConstructor() call. If I did that, it would fail my original requirement that the logic is run before anything else in the life-cycle of this component. Still... it's a fairly decent approximation of a "constructor" - even if that functionality is dependent upon where I place the call in the function body.

For this reason, it might be more intuitive to rename useConstructor() to useSingleton(). Because that's what it does. It ensures that a given block of code is run once, and only once. If you then place that logic at the very top of your function declaration, it is, effectively, a "constructor", for all intents and purposes.

Discussion (28)

pic
Editor guide
Collapse
jz222 profile image
Timo Zimmermann • Edited

In this case, I would use useRef instead of useState to omit an unnecessary render cycle, even though it's a small one.

const useConstructor = (callBack = () => {}) => {
  const hasBeenCalled = useRef(false);
  if (hasBeenCalled.current) return;
  callBack();
  hasBeenCalled.current = true;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
bytebodger profile image
Adam Nathaniel Davis Author

Good point! I haven't really messed around enough with useRef(). This works better.

Collapse
molinch profile image
Fabien Molinet • Edited

Hey @bytebodger I am new to hooks, I previously only used components where I used to rely on the constructor. I wonder if I'm doing it right... since I really miss the constructor functionality, I created two hooks: useCreateOnce (returns a value) and useRunOnce.

But now I'm using these 2 in many places so I wonder if there isn't another way, which follows more React hooks "intended" philosophy? Or maybe I shouldn't worry that much?

Thread Thread
bytebodger profile image
Adam Nathaniel Davis Author • Edited

dev.to/spukas/4-ways-to-useeffect-pf6

This article right here on Dev.to is pretty handy - so much so that I stuck it in my reading list. A constructor is ultimately a lifecycle method. And in Hooks, there is exactly one lifecycle method - useEffect(). As the name implies, there is no such thing as a useEffect() call that runs before the component renders (since, an "effect" has to happen... after something else). And as the article above implies, useEffect() can actually be used four different ways - although I don't personally believe that any of them are intuitive just by reading the names/arguments.

Collapse
wolverineks profile image
Kevin Sullivan

would lazy initialization of useState work as a useSingleton
e.g.

const useSingleton = (initializer) => {
  React.useState(initializer)
}

const Foo = () => {
  useSingleton(() => {
    // only runs once
  })
  ...
}
Collapse
peerreynders profile image
peerreynders • Edited

A more fleshed out example:

// custom hook "use initializer"
function useInit(init, ...args) {
  const tuple = useState(null);
  const [state, setState] = tuple;

  if(state !== null) {
    return tuple;
  }

  const initState = init(setState, ...args);
  setState(initState);
  return [initState, setState];
}

// Component Parts
function initCounter(setState, startAt) {
  const increment = state => ({...state, count: state.count + 1});
  const clickHandler = () => setState(increment);

  return {
    count: Number(startAt),
    clickHandler
  };
}

// Component
function Counter({startAt}) {
  const [{count, clickHandler}, setState] = useInit(initCounter, startAt);

  return html`
    <p>
      <button onClick=${clickHandler}>Click me</button>
    </p>
    <output>You clicked ${count} times</output>
  `;
}

function App() {
  return html`<${Counter} startAt="12" />`;
}

Gist

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

Yep. That works as well!

Collapse
isaachagoel profile image
Isaac Hagoel

For me the fact that all of the hooks are invoked every time and have to bail out if they don't need to run (basically by using an "if" statement explicitly like you've demonstrated or implicitly using dependency arrays) is hugely offensive. So much unneeded work and wasted electricity plus wasted brain cycles (should it run every time or not...)
The virtual Dom is already the king of wasted CPU cycles (diffing all of the things when barely anything has changed) but at least it doesn't put a constant mental tax on the developer. Until you need to start manually preventing it from updating every time that is.

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

I think you know this already, but I couldn't agree with you more. One (perfectly valid) response to this entire post would be, "Dooood, if you're so in love with constructors, then just write class-based components." But as (I assume) you've experienced already, sometimes you find yourself on a project / assignment / whatever where Pattern X is already the "accepted norm". And you're thinking, "Well... I usually use Pattern Y, which has Feature A. But, I don't see any analog for Feature A in this new pattern."

For the first time since they were introduced in Oct 18, I find myself on a team where we are (and plan to continue) cranking out functional components - with lots-and-lots of Hooks. So, in that environment, I'm trying to figure out the best way to adapt to "Pattern X" (in this case, Hooks) without losing some of the key features that I enjoyed in "Pattern Y" (in this case, class-based components).

I'm also trying, oh-so-hard, to not be that guy. You know that guy. He's the one who's always done things a "certain way", and when the team says, "No, we're gonna do this new project in a different way," he sits in the corner and pouts cuz he really just wants to do everything the same way he's always done it in the past.

Collapse
devhammed profile image
Hammed Oyedele

I have used hooks in up to 3 production projects but I can't find the use cases of this hook honestly.

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

Haha - well, I'm not trying to convince you to use it. I'm just pointing out some ways that you could achieve it if you do find the use case.

I don't know if I've ever had the occasion to actually write my own closure. That doesn't mean that there aren't use-cases to write closures.

Collapse
devhammed profile image
Hammed Oyedele

Okay and funny enough, I use closures a lot of time e.g in factory functions.

Thread Thread
bytebodger profile image
Adam Nathaniel Davis Author

And that's why I said that I consider the Hooks documentation regarding constructors to be arrogant. It starts from the (false) assumption that there's only one reason to ever need/use a constructor (to initialize state) and then it doubles down on that folly by stating that there's simply no need for them anymore.

With closures, I'm sure there are many devs out there like me who aren't using them often (or at all) in their code. That doesn't mean that we should make a global statement that they're not needed. And we definitely shouldn't do anything to remove support for them.

Thread Thread
peerreynders profile image
peerreynders • Edited

Ryan Carniato (SolidJS, MarkoJS) notes in: React Hooks: Has React Jumped the Shark? (Feb 2019):

The challenge here is, to use Hooks beyond simple scenarios requires you to be a competent JavaScript developer. And I don’t mean this in any way a slight. But if you are having issues with this or bind with Classes, Hooks are no simpler. Hooks force you to really understand closures, reference vs value types, and memoization, and more importantly have a keen eye (or linter) to make sure you do not miss any small piece required to make them work.

He has a number of other pro-Hook (but not fanboyish) articles:

That last one explains where hooks do make sense.

The fit with React at this point is somewhat awkward - one has to wonder whether React is maneuvering into a position where ditching the VDOM becomes an option (to replace it with modern Fine-Grained change detection).

Thread Thread
bytebodger profile image
Adam Nathaniel Davis Author

Oooh... these links look awesome. In particular, I love love LOVE the assessment that "Hooks are no simpler". I think this speaks to some of the frustration I've had with them, literally since the day they were announced. I'm not anti-Hooks. In fact, all of my current React dev is now Hooks-based. But I'm continually annoyed by some fanboys who act as though Hooks are self-evidently better, and everything else is clearly worse. They act as though the answer to, "Why Hooks?" is "Well... Hooks!!!"

The historical perspective in these articles is also awesome. I've made a few references in my previous articles to Knockout. It's amazing (to me) how many "senior" JS devs today don't even know what Knockout was/is. This author seems to have a very well-rounded approach to the whole thing.

Thread Thread
peerreynders profile image
Collapse
wolverineks profile image
Kevin Sullivan

Could you give an example where a constructor would be useful?

Collapse
alexvukov profile image
alex-vukov

Unfortunately using a constructor seems to be the only way to go if you are using Redux and you don't want to blink a "no data" message or stale data before a fetch action is dispatched in useEffect. Having worked with other frameworks, I am simply amazed by having to do this ugliness in React...

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

Interesting/related note: This article has quietly gathered more views than anything else I've written. Unlike most of my articles, that have a certain "splash" for a few days - and then are rarely read again - this article continues to get a steady stream of traffic.

What's the point?? Well, it would seem that there are a lot of other people out there who are switching to Hooks, then saying, "Wait a minute. How do I create a constructor with Hook??", then they start googling and end up on my article. (And there seems to be very few other articles out there on the same topic.)

So while it may indeed be "ugliness", I surmise that there are many others who are confused/annoyed by the (FALSE) idea that you can simply use useEffect() to replace all of the previous lifecycle methods, and there's no logical reason to use the constructor functionality that we had in class-based components.

Thread Thread
alexvukov profile image
alex-vukov

Thank you for writing this article! I thought I was missing something by always having to skip renders with a flag which is set only after a fetch is started in useEffect or trying to do fetch calls in something like your constructor. Seems that React devs have missed the point that sometimes you need to have a cause before you can handle the effect...

Thread Thread
bytebodger profile image
Adam Nathaniel Davis Author

Having spent years working with the lifecycle methods that are available in class-based components, I'll be the first to tell you that they can, at times, cause major headaches. But the Hooks team seems to have decided that the way to cure the headache is to cut off the head.

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

The easiest analogy is componentWillMount. Anything that anyone previously wanted to stuff in componentWillMount is a prime candidate to be put in the constructor. But don't take my word for it. This is directly from the React docs:

UNSAFE_componentWillMount() is invoked just before mounting occurs. It is called before render(), therefore calling setState() synchronously in this method will not trigger an extra rendering. Generally, we recommend using the constructor() instead for initializing state.

So if you were using old-skool React, you might've had a handful of components that used componentWillMount(). Then, that was deemed to be unsafe, and the maintainers themselves said, "Now you should move it to the constructor(). But with function-based components, there is no constructor.

Again, as I stated in the post, I'm not claiming that this is functionality that you'd need on all components. You won't even need it on most components. But the fact that we previously had componentWillMount() and constructor() signals that there are valid use-cases for it.

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

I will add here that, even in the documentation for class-based components, there's a general assumption on the part of the React team that constructor functionality is limited to initializing state. In my personal experience, I know that, on numerous occasions, I've found it prudent to handle other bits of logic in this part of the component life-cycle. Specifically, there are times when I want to prepare variables that will be used in the component, but I don't want the updating of those variables to be tied to state. In other words, there are sometimes some variables that I want to live outside of the rendering process. When I need to do any pre-processing on said variables, the constructor is an ideal place to do so.

Collapse
jackmellis profile image
Jack • Edited

I often face the same issue but it is easily solved with a useRef. I usually write a useEagerEffect hook that runs before rendering, but still only runs when the deps array changes.

The part I really agree with, though, is how react's docs (and those of many other libraries) make these sweeping assumptions that nobody could ever possibly need anything outside of their perceived use cases. The blind arrogance you mention really frustrates me!

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

You're spot-on about the useRef. In fact, in my useConstructor NPM package, I wrote it with useRef, rather than the useState approached illustrated in the article. There was another commenter on this thread that was gracious enough to point me in that direction.

Collapse
eliasmqz profile image
Anthony Marquez

I just wanna say this is a great article, thanks for this really helped me out with solving the lack of a constructor use case.

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

Thank you - I'm glad it helped!

Collapse
ronakpanchal93 profile image