You can see where it's set up in the React source code. Outside of a mounted component, the standard hooks (useEffect
, useState
etc) are initialised to throwInvalidHookError
, which does the obvious thing.
Within a mounted function component, useEffect
is set to the mountEffect
function which does the expected effect-running stuff.
Custom hooks aren't treated specially (although the standard linting rules will report any abuses), but a custom hook that doesn't call any of the standard hooks – either directly or indirectly – doesn't need to be a hook at all. If it does call one of the standard hooks then, obviously, the Invalid hook call
exception will be thrown.
why must it be an error to try and do this
It needs to be an error because the standard hooks have special treatment inside the React machinery. The "rules of hooks" result in a consistent ordering of hooks within each component; they are enforced so that React can maintain internal state associated with hook call.
This is a design choice by the authors of React. Some limitations are put in place, it results in optimisations in the implementation.
Per my understanding these are simply JS functions that return a React Element
Yes, components are just JS functions that return a representation of a React node tree. React then converts this node tree into HTML nodes with associated event handlers etc.
Is there no way to do this other than extract every hook dependency and pass in a stubbed implementation?
It's not entirely clear what you're trying to do here, but the short answer seems to be "correct, there is no way without restructuring the code". You can either use regular functions to call regular functions, or use hooks to call hooks (and regular functions). There's no magic to it – you can make your own function into a hook just by starting its name with use
, then it can call other hooks, as long as it follows the rules of hooks.
EDIT
I simply would like to invoke the component like so T().
It's still not clear what problem you're hoping to solve by doing that. The return value from a component is a data structure reflecting the component tree. You can see for yourself by writing a hookless component:
const HelloWorld = () =>
<div>Hello <span id="world">World</span></div>
console.log(HelloWorld());
Sandbox
{
"type": "div",
"key": null,
"ref": null,
"props": {
"children": [
"Hello ",
{
"type": "span",
"key": null,
"ref": null,
"props": {
"id": "world",
"children": "World"
},
"_owner": null,
"_store": {}
}
]
},
"_owner": null,
"_store": {}
}
So, it's a familiar tree structure with some extra slots for React housekeeping data.
We can explore a little further by calling HelloWorld()
from a mounted component:
const App = () => {
console.log(HelloWorld());
return <div></div>
}
...
root.render(<App/>);
Sandbox
This renders an empty <div></div>
, but the return value from HelloWorld()
contains some new innards:
{
"type": "div",
"key": null,
"ref": null,
"props": { "children": ... },
"_owner": FiberNode,
"_store": {}
}
The FiberNode
value hints at what React is doing behind the scenes – Fiber is the algorithm that does what React does, FiberNode
is an internal data structure it uses to do that.
What happens if we add a hook call to HelloWorld()
?
const HelloWorld = () => {
useEffect(() => console.info("Howdy!"));
return <div>Hello <span id="world">World</span></div>
}
Sandbox
Of course if we call this directly then we get the familiar Invalid hook call
error, but if we call it from a mounted component ... we see Howdy!
on the console output. Interesting ....
This works because, as you mentioned, these are all just plain old Javascript functions. Calling useEffect()
from HelloWorld()
from <App/>
does the same work as calling useEffect()
directly from <App/>
. This is not, however, the same as calling useEffect()
from <HelloWorld/>
.
Let's try something else. What happens if we create a <HelloWorld/>
element?
const App = () =>
{
console.log("<HelloWorld/>", <HelloWorld/>);
return <div/>
}
...
render(<App/>);
Sandbox
We no longer get a Howdy!
, and the return value is different:
<HelloWorld/>
{
"type": f HelloWorld() {}
"key": null,
"ref": null,
"props": {},
"_owner": FiberNode,
"_store": {}
}
Very interesting. When the function component is referenced via JSX, the function does not get called. <HelloWorld/>
is just syntactic sugar for calling createElement(HelloWorld)
, and the function only gets called when that element is rendered.
Comparing the return values, we also see that the children are no longer visible from the outside, and the tree structure is just a single HelloWorld
node rather than a nested div
/ span
/ text structure.
Let's try one more thing. What if we only call HelloWorld
some of the time?
const App = () => {
const [count, setCount] = useState(1);
return (
<div onClick={() => setCount((c) => c + 1)}>
Click #{count}
<hr />
{count <= 1 &&
/* render HelloWorld component */
<HelloWorld /> }
{ count <= 2 &&
/* call HelloWorld function */
console.log("HelloWorld()", HelloWorld()) }
</div>
);
};
Sandbox
Clicking on the <div>
will update the count
, and we'll call different things each time.
On first load, we see Hello World!
rendered in the browser, and the console shows us:
Howdy!
Howdy!
HelloWorld()
is invoked twice (which gives us the two greetings) – once from it being rendered, and once from the direct call.
Clicking to get a second render, we see:
Howdy!
We are no longer rendering <HelloWorld/>
, so we only see the greeting from our direct call to HelloWorld()
.
Clicking to get a third render ...
Rendered fewer hooks than expected.
Aha! We broke the rules of hooks and React complained about it. The first two times we rendered <App/>
, the useEffect()
hook was called, but the third time it wasn't. Hooks can't be called conditionally.
But ... it didn't complain when we rendered <HelloWorld/>
conditionally. That's the same function calling the same hook! This is outrageous, it's unfair, etc Each component instance gets its own ledger for keeping track of hooks. When we call HelloWorld()
directly, the useEffect()
is tracked in the App
instance. When we render <HelloWorld/>
, the useEffect()
is tracked in the HelloWorld
instance. If we render <HelloWorld/>
multiple times, each instance tracks its own useEffect()
call.
Apologies for the lengthy but not-particularly-comprehensive peek behind the curtains of React function components and effects. I hope it went some way to address your open-ended "why must it be an error to try and do this?" / "what am I missing?" questions.
The really short answer is "when you put <Foo/>
in your component tree, you're not calling it directly".
In terms of your original premise: "Out of curiosity I wanted to call a React function component directly", you found out what happens. React complains! If you have a packhorse and you remove oxygen and gravity, it doesn't work too well either.