junlin
junlin

Reputation: 2035

React: what's the difference between a function returning JSX and function component?

function renderSomething(foo) {
  return <div>sth {foo}</div>
}

function Something({ foo }) {
  return <div>sth {foo}</div>
}

function Component(props) {
  const { foo } = props
  return (
    <>
        {renderSomething(foo)}
        <Something foo={foo} />
    </>
  )
}

The JSX result of renderSomething() and <Something /> is identical. I wonder what's the difference(e.g. render way, behavior, influence, etc) between these two ways?

And what applicable scenario for render method(i.e. renderSomething())? Can I use hooks inside?

Upvotes: 22

Views: 6943

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074335

The JSX result of renderSomething() and <Something /> is identical.

renderSomething(foo) and <Something foo={foo} /> do quite different things, but in that specific usage, the end result is the same: React creates a React element that tells it to render a div later (if the element is used). Doing renderSomething(foo) is treating renderSomething like a hook, which has implications for how you use it when when it runs.

Here are two key differences:

  1. With renderSomething(foo), your code is calling the function immediately and passing in the argument. With <Something foo={foo} />, you aren't calling Something. You're asking React to remember it and call it later if/when it needs to render the component where you did these calls.

  2. Because your code, not React, calls renderSomething, if you used hooks in it, they'd be putting information in the calling component's instance and state information. In contrast, if Something uses hooks, that instance and state information is stored in a component instance for Something itself.

(There are other differences. You can't use renderSomething as a component via <renderSomething foo={foo} /> because function component names must start with upper case to differentiate them from HTML elements, and because renderSomething expects a string argument, not a props object.)

Let's look at a couple of those differences more closely:

function renderSomething(foo) {
    console.log(`renderSomething: Called with foo = ${foo}`);
    return <div>sth {foo}</div>;
};

function Something({ foo }) {
    console.log(`Something: Called with foo prop = ${foo}`);
    return <div>sth {foo}</div>;
}

console.log(`Creating elements via renderSomething("x"):`);
const ex1 = renderSomething("x");
console.log(`Creating elements via <Something foo="x" />:`);
const ex2 = <Something foo="x" />;
console.log(`Done`);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Notice that the code called renderSomething (you see its console.log occur), but didn't call Something (you don't see its console.log). That's because of difference #1 above. The key thing is that a component's function is called only if it needs to be rendered. We haven't rendered either result above, so the function is never called.

The second difference is more subtle, but let's suppose two things:

  1. Your "something" needs an effect (maybe it loads something via the network) (or it could be state or a ref or anything else that it used a hook for), and
  2. The component using it (let's call it Example) may or may not need to render it.

That works just fine with Something:

const { useState, useEffect } = React;

function Something({ foo }) {
    const [thing, setThing] = useState(null);
    useEffect(() => {
        setTimeout(() => {  // Fake ajax
            setThing("thing"); 
        }, 100);
    }, []);
    return <div>foo = {foo}, thing = {thing}</div>;
}

function App() {
    const [foo, setFoo] = useState("");

    return <div>
        <div>
            <label>
                <input type="checkbox" checked={!!foo} onChange={() => setFoo(foo => foo ? "" : "hi")} />
                Toggle "foo"
            </label>
            {foo ? <Something foo={foo} /> : null}
        </div>
    </div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
label {
    user-select: none;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

But now let's try it with renderSomething:

const { useState, useEffect } = React;

function renderSomething(foo) {
    const [thing, setThing] = useState(null);
    useEffect(() => {
        setTimeout(() => {  // Fake ajax
            setThing("thing"); 
        }, 100);
    }, []);
    return <div>foo = {foo}, thing = {thing}</div>;
}

function App() {
    const [foo, setFoo] = useState("");

    return <div>
        <div>
            <label>
                <input type="checkbox" checked={!!foo} onChange={() => setFoo(foo => foo ? "" : "hi")} />
                Toggle "foo"
            </label>
            {foo ? renderSomething(foo) : null}
        </div>
    </div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
label {
    user-select: none;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

It blows up when you tick the box with this error:

Warning: React has detected a change in the order of Hooks called by App. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks

   Previous render            Next render
   ------------------------------------------------------
1. useState                   useState
2. undefined                  useState
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    at App (<anonymous>:35:22)

That's because that makes the code call renderSomething when we didn't call it on the first render. That's because of #2 above: Since renderSomething isn't a component function, it doesn't get its own component instance, and any calls to hooks within it are just like calls to hooks in the parent component (Example). But function components aren't allowed to call hooks sometimes and not other times. That's because React relies on the order in which hooks are called, and that order has to be consistent for it to do its hook management. Again, using renderSomething like that is using renderSomething as a hook (this is exactly how custom hooks work, by ending up calling built-in hooks, which then store the information in the parent instance).

So as you can see, while the result you got in your example was the same, in general they're quite different things. :-)

Upvotes: 39

Related Questions