Does this count as breaking the rule of hooks? It doesn't show the error/warning 'Error: Invalid hook call' and the code works normally. useEffect
also mounts only once
1.✅ Call them at the top level in the body of a function component.
2.✅ Call them at the top level in the body of a custom Hook.
3.🔴 Do not call Hooks inside conditions or loops.
4.🔴 Do not call Hooks after a conditional return statement.
5.🔴 Do not call Hooks in event handlers.
6.🔴 Do not call Hooks in class components.
7.🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect. https://react.dev/warnings/invalid-hook-call-warning
If it does break the rule of hooks, what's the drawback of using it this way? It would be great if there's a counter example of why this case it an anti-pattern. Thank you very much!
Here's the playground: https://codesandbox.io/p/sandbox/goofy-mcclintock-pchv3f
import { useState, useEffect } from "react";
const App = () => {
const [name, setName] = useState("");
console.log("rerender");
return (
<div>
<h1>Hello World</h1>
<input
type="text"
placeholder="name"
value={name}
onChange={(e) => setName(e.currentTarget.value)}
/>
<HookContainer
useHooks={() => {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log("HookContainer mount");
}, []);
return {
counter,
setCounter,
};
}}
>
{({ counter, setCounter }) => (
<div>
<p>counter: {counter}</p>
<button
onClick={() => {
setCounter((prev) => prev + 1);
}}
>
increment
</button>
</div>
)}
</HookContainer>
</div>
);
};
type HookContainerProps<T> = {
children: (props: T) => JSX.Element;
useHooks: () => T;
};
const HookContainer = <T extends object>(props: HookContainerProps<T>) => {
const hookData = props.useHooks();
const Element = props.children;
return <Element {...hookData} />;
};
export default App;
------ New Section ---------
Seems like this way also works. The state is also still in order (according to Robin Zigmon comment)
import React, { useState, useEffect } from 'react';
const arr = Array.from({ length: 3 })
export function App(props) {
const [name, setName] = useState('')
return (
<div className='App'>
<input type="text" value={name} onChange={e => setName(e.currentTarget.value)} />
{name && arr.map((_, index) => <ComponentWrapper key={index}>
{() => {
const [counter, setCounter] = useState(0)
useEffect(() => { console.log('init') }, [])
return <div>
<p>{counter}</p>
<button onClick={() => { setCounter(prev => prev + 1) }}>increment</button></div>
}}
</ComponentWrapper>)}
</div>
);
}
const ComponentWrapper = (props) => {
return props.children()
}
IIUC, this is simply an extra abstraction layer around a custom Hook, that provides props for a Function-as-a-Child, and works without breaking any rule:
useHooks
is a custom Hook function<HookContainer>
Component (as you already realized in your point 2)While this abstraction may look strange, I do not see technical issues with it, except for the Function-as-a-Child (FaaC) render prop which is instantiated as a Component (<Element/>
), whereas it should just be called as a simple function ({Element(hookData)}
): because the callback you pass as FaaC is inlined, it is a new function at each render, hence React destroys and re-creates its DOM nodes everytime, instead of re-using them (to avoid browser re-layout and re-paint, which is the whole point of React Virtual DOM). See https://stackoverflow.com/a/78743482/5108796 and https://kentcdodds.com/blog/dont-call-a-react-function-component (for the reverse reason: keep the inlined FaaC as a "JSX sub-template", in the same context as the <HookContainer>
Component, instead of rendering it as a new Component at each render). Or you could memoize it.