[NOTE: I've since written an update to this article with an improved solution. It can be read here: https://dev.to/bytebodger/streamlining-constructors-in-functional-react-components-8pe]
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>
);
};
}
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>
);
};
}
As you see, there's no need for a constructor simply to initialize your state variables, unless you have to initialize the state variables based upon the props. If this isn't necessary, you can declare initial state directly inside 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 theuseState
call. If computing the initial state is expensive, you can pass a function touseState
.
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.
Code that runs before anything else in the life-cycle of this component.
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("Occurs EVERY time the component is invoked.");
return (
<>
<div>Counter: {counter}</div>
<div style={{ marginTop: 20 }}>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
</div>
</>
);
};
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(
"Occurs ONCE, AFTER the initial render."
);
}, []);
console.log("Occurs EVERY time the component is invoked.");
return (
<>
<div>Counter: {counter}</div>
<div style={{ marginTop: 20 }}>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
</div>
</>
);
};
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(
"Occurs ONCE, AFTER the initial render."
);
}, []);
useLayoutEffect(() => {
console.log(
"Occurs ONCE, but it still occurs AFTER the initial render."
);
}, []);
console.log("Occurs EVERY time the component is invoked.");
return (
<>
<div>Counter: {counter}</div>
<div style={{ marginTop: 20 }}>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
</div>
</>
);
};
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(
"Occurs ONCE, AFTER the initial render."
);
}, []);
useLayoutEffect(() => {
console.log(
"Occurs ONCE, but it still occurs AFTER the initial render."
);
}, []);
const constructor = () => {
if (constructorHasRun) return;
console.log("Inline constructor()");
setConstructorHasRun(true);
};
constructor();
console.log("Occurs EVERY time the component is invoked.");
return (
<>
<div>Counter: {counter}</div>
<div style={{ marginTop: 20 }}>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
</div>
</>
);
};
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(
"Occurs ONCE, BEFORE the initial render."
);
});
const [counter, setCounter] = useState(0);
const [constructorHasRun, setConstructorHasRun] = useState(false);
useEffect(() => {
console.log(
"Occurs ONCE, but it occurs AFTER the initial render."
);
}, []);
useLayoutEffect(() => {
console.log(
"Occurs ONCE, but it still occurs AFTER the initial render."
);
}, []);
const constructor = () => {
if (constructorHasRun) return;
console.log("Inline constructor()");
setConstructorHasRun(true);
};
constructor();
console.log("Occurs EVERY time the component is invoked.");
return (
<>
<div>Counter: {counter}</div>
<div style={{ marginTop: 20 }}>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
</div>
</>
);
};
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(
"Occurs ONCE, 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>
</>
);
};
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.
Top comments (36)
In this case, I would use
useRef
instead ofuseState
to omit an unnecessary render cycle, even though it's a small one.Good point! I haven't really messed around enough with
useRef()
. This works better.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?
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 auseEffect()
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.would lazy initialization of useState work as a
useSingleton
e.g.
A more fleshed out example:
Gist
Yep. That works as well!
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.
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.
Could you give an example where a constructor would be useful?
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...
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 useuseEffect()
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.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...
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.
I'm always willing to learn different ways and adapt, but i used to use a react constructor to pre-prep a lot of data, even for static websites. For instance i might translate a linear array into a 2d one better suited for defining a layout. Things like that. I guess i could do it another way but they all just seem like added complexity.
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.
The easiest analogy is
componentWillMount
. Anything that anyone previously wanted to stuff incomponentWillMount
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: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 theconstructor()
. 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()
andconstructor()
signals that there are valid use-cases for it.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!
You're spot-on about the
useRef
. In fact, in myuseConstructor
NPM package, I wrote it withuseRef
, rather than theuseState
approached illustrated in the article. There was another commenter on this thread that was gracious enough to point me in that direction.I have used hooks in up to 3 production projects but I can't find the use cases of this hook honestly.
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.
Okay and funny enough, I use closures a lot of time e.g in factory functions.
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.
Ryan Carniato (SolidJS, MarkoJS) notes in: React Hooks: Has React Jumped the Shark? (Feb 2019):
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).
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.
I needed Deep dive: How do React hooks really work? (2019) before hooks made sense to me.
I appreciate this half rant, half instruction. Some of the workarounds to move to a hooks only react environment really are not all that clean or efficient.
Also just FYI: unless i'm mistaken and there is yet another way to declare functions, your const useConstructor variable is never initialized as a function.
In both cases where I illustrated
useConstructor
, it was defined above theApp
function.Yeah it's where you defined it that i don't quite understand and it throws errors for me. "Const declarations' require an initialization value."
Your one has:
Should it not be this? (which works)
I'm not trying to nitpick... i'm just uncertain of my own knowledge and the ever changing landscape of javascript.
Ahhh, yes. Good catch. I am missing the
=>
. Obviously, I didn't actually run these examples. I just typed them out. Thanks!I had a problem related to this topic: I needed a unique-id in my component. In order to avoid generating it on every update I did this initially:
However, that didn't help. The value of
uniqId
remained the same throughout all the updates, but the call togenerateUniqId()
was made on every update. I solved it like this:That made sure that
generateUniqId()
was called only once and not on every update.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.
Thank you - I'm glad it helped!
What about useMemo? It seems to work, what are the tradeoffs? Would it solve the constructor issue?