javascriptreactjsreact-hooks

React hooks: Is it advisable to call components as functions?


Consider we have this example component:

function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <div> {count} </div>

      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increase
      </button>
    </div>
  );
}

I have sometimes come across code where someone calls a react component as function in render:

 const App = () => (
     <div> {Counter()} </div>
 )

vs rendering it as element:

 const App = () => (
     <div> <Counter/> </div>
 )

In react hooks, is it allowed to call components as functions? What can go wrong if we do so?


There seems to be a question which can be considered similar to this one; although that one is not specifically about hooks. Also it seems OP there is interested in specific things such as performance implications for example.


Solution

  • React docs discourage calling components as functions:

    Components should only be used in JSX. Don’t call them as regular functions. React should call it.

    One of the reasons they mention for this is:

    If a component contains Hooks, it’s easy to violate the Rules of Hooks when components are called directly in a loop or conditionally.

    Although they don't go into details with above quote, I think below example shows what they might mean.

    When you call a component as a function and it contains usage of hooks inside it, in that case react thinks the hooks within that function belongs to the parent component. So if you conditionally call such component (as we do with TestB() below) you will violate one of the rules of hooks. Click the re-render button to see the error:

    Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

     
    function TestB() {
      let [B, setB] = React.useState(0);
      return (
        <div
          onClick={() => {
            setB(B + 1);
          }}
        >
          counter B {B}
        </div>
      );
    }
     
    
    function App() {
      let [A, setA] = React.useState(0);
    
      return (
        <div>
          <button
            onClick={() => {
              setA(A + 1);
            }}
          >
            re-render
          </button>
          {/* Conditionally render TestB() */}
          {A % 2 == 0 ? TestB() : null}
        </div>
      );
    }
    ReactDOM.render(
      <App />,
      document.getElementById("react")
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="react"></div>

    Now you can use <TestB/> above instead and see the difference.

    Docs also mention that rendering component as element (as opposed to calling it as function) allows component types to participate in reconciliation.

    This means that when you render a react component as react element say <TestB/> and then on next render you render some different component <TestC/> instead of it (in the same place in component hierarchy), due to reconciliation algorithm (and since component type has changed), react will unmount <TestB/> component (all its state will be gone) and mount a new component <TestC/> instead. If you call it as function however (e.g. TestB()), the component type will not participate in reconciliation anymore and you might not get expected results.

    See below example:

    function TestB() {    
      return (
        <div     
        >
          <input/>
        </div>
      );
    }
    function TestC() {
      console.log("TestC")
      return (
        <div     
        >
          <input/>
        </div>
      );
    }
    
    function App() {
      let [A, setA] = React.useState(0);
    
      return (
        <div>
          <button
            onClick={() => {
              setA(A + 1);
            }}
          >
            re-render
          </button>
          {/*  Here we are alternating rendering of components */}
          {A % 2 == 0 ? TestB() : TestC()} 
        </div>
      );
    }
    
    ReactDOM.render(
      <App />,
      document.getElementById("react")
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
        <div id="react"></div>

    Render these components as elements now (<TestB/> and <TestC/>) to see the difference.

    Benefits of rendering component as element

    Docs also mention following benefits of rendering component as element:

    Components become more than functions. React can augment them with features like local state through Hooks that are tied to the component’s identity in the tree.

    Component types participate in reconciliation. By letting React call your components, you also tell it more about the conceptual structure of your tree. For example, when you move from rendering to the page, React won’t attempt to re-use them.

    React can enhance your user experience. For example, it can let the browser do some work between component calls so that re-rendering a large component tree doesn’t block the main thread.

    A better debugging story. If components are first-class citizens that the library is aware of, we can build rich developer tools for introspection in development.

    More efficient reconciliation. React can decide exactly which components in the tree need re-rendering and skip over the ones that don’t. That makes your app faster and more snappy.