Search code examples
javascriptreactjsreact-hooks

React hooks: call component as function vs render as element


Say we have component:

let Component = (props)=><div>Hi</div>;

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

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

vs rendering it as element

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

In react hooks, what can be the consequences of calling component as function?


There seems to be a question which can be considered similar to this one; although that one is not specifically about hooks. Also the OP asks some specific things he is interested in like performance implications, etc.


Solution

  • Here are some things which can happen if you call component as function vs rendering it as element.

    1. Potential violation of rules of hooks

    When you call a component as a function (see TestB() below) and it contains usage of hooks inside it, in that case react thinks the hooks within that function belongs to the parent component. Now if you conditionally render that component (TestB()) you will violate one of the rules of hooks. Check the example below, 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/> instead and see the difference.

    1. Reconciliation might not work as expected

    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:

    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>

    • Type something in the input
    • Now click the re-render button
    • You can see now from the log that component TestC was rendered, but the input shows the same value you typed before - which might not be what you want as you rendered a different component. This happened because reacts reconciliation algorithm couldn't detect that we moved to a different component (from TestB to TestC) and didn't remove previous input instance from DOM.

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