Components with uppercase function names are rendered multiple times in React Hooks

Viewed 325

I've been developing with Hooks for a long time, But I met a situation where I was confused.

Let me briefly describe this strange problem with a particular case.

I created a very simple functional component called Test:

// test.js
export default function Test () {

  useEffect(() => {
    console.log('Test')
  }, [])

  return (
    <h2>
      Test Component
    </h2>
  )
}

It will print something at the first render

And the component will be referenced in App

// App.js
import React, { useEffect, useState } from "react";
import Test from './test';

export default function App() {
  const [number, updateNumber] = useState(0);

  const renderComponent = () => {
    return (
      <Test />
    )
  }

  const RenderComponent = () => {
    return (
      <Test />
    )
  }

  useEffect(() => {
    updateNumber(1)
    updateNumber(2)
  }, [])

  console.log('every-render')

  return (
    <div className="App">

      {renderComponent()}

      {/* <RenderComponent /> */}

      <h1>Rerender times: {number}</h1>
    </div>
  );
}

So something strange happened

When I use this way, the Test component will be printed once

// Case 1
const renderComponent = () => {
    return (
      <Test />
    )
}

// ...

renderComponent()

Something that it prints in Console

But when I use this way, the Test component will be printed twice

// Case 2
const RenderComponent = () => {
    return (
      <Test />
    )
}

// ...

<RenderComponent />

Something that it prints in Console

Of course, I wrote a demo for you to debug: https://codesandbox.io/s/demo-for-hooks-8gkml?file=/src/App.js

I am confused why in case 2 the Test component will be remounted each time the Hooks are rerendered.

2 Answers

Every render cycle the RenderComponent component is recreated, so it is mounted every render, and thus, mounts its children. Consider the following code where you render <Test /> directly, it's output is identical to {renderComponent()}.

export default function App() {
  const [number, updateNumber] = useState(0);

  const renderComponent = () => {
    return <Test />;
  };

  const RenderComponent = () => {
    return <Test />;
  };

  useEffect(() => {
    updateNumber(1);
    updateNumber(2);
  }, []);

  useEffect(() => {
    console.log("every-render");
  });

  return (
    <div className="App">
      {/* {renderComponent()} */}
      {/* <RenderComponent /> */}
      <Test /> // <-- "Mounted Test", "every-render", "every-render"

      <h1>Rerender times: {number}</h1>
    </div>
  );
}

The function renderComponent is executed and the return value is <Test />, thus equivalent to just rendering <Test /> directly, whereas RenderComponent is a new component each cycle.

Edit weird rendering issue (maybe, maybe not)

When diffing two trees, React first compares the two root elements. The behavior is different depending on the types of the root elements.

Consider 3 cases:

{renderComponent()}
<RenderComponent />
<Test />

1) {renderComponent()}

Every render just return Test type, algorithm compares and sees that the type has not changed. Result: mount Test component 1 time.

2) <RenderComponent />

Every render return new component type RenderComponent, algorithm compares and sees RenderComponent1 type != RenderComponent2 type, and:

Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch.

Result: mount child Test component 2 times.

Add console to mount RenderComponent:

const RenderComponent = () => {
    useEffect(() => console.log('RenderComponent mount'), []);
    return <Test />;
};

RenderComponent too mount 2 times.

3) <Test />

Similarly 1 case. Result: mount Test component 1 time.

Related