What is the evaluation order for JSX in React?

Viewed 365

Suppose I have the following components:

const Cell = (props) => {
    const { cellData } = props;
    console.log("--START--")
    console.log(cellData);
    console.log("--END--")
    return <div>cellData</div>;
};

const Col = (props) => {
    const { cell } = props;
    console.log(cell);
    return (<div>    </div>);
}

Doing something like

    const col = (<Col 
        cell={<Cell />}
    />);

    const content = (
        <div>
            {col}
        </div>
    );

Seems to run the code in Col first. But if props is just a function parameter, shouldn't it run the <Cell /> part first that was passed in?

2 Answers

For what it's worth this is a trivial matter to test. What you see and what you ask about are two sides of the same process.

Issue

When you console log in the body of a functional component this is done as a side-effect.

enter image description here

The function body of a functional component is essentially the equivalent of a class-based component's render method. In other words, the entire function is the "render" method. React "renders" each component as part of the reconciliation process to compute a diff from any one render cycle to the next. Note also that the "render" function is to be considered a pure function, free of any side-effects, like console logging or any other asynchronous behaviors.

You ask about the "evaluation order" and what you think you should see is the "render order". In other words, React must evaluate the component tree from the top down, in a depth-first traversal, but it renders to the DOM from the bottom up. It needs to have fully processed all children in any subtree to compute a diff of that subtree.

In React we generally do not care about anything until a component is mounted and usable as this is when the component can "work with the DOM, run side-effects, and schedule updates". This is why mounting order is generally more important than evaluation order. Evaluation can be "paused, aborted, or restarted" by React.

With the provided code, modified to console log when the component is rendered into the DOM:

const Cell = ({ cellData }) => {
  useEffect(() => {
    console.log("Cell MOUNTED");
    console.log("--START--");
    console.log({ cellData });
    console.log("--END--");
  }, []);
  return <div>{cellData}</div>;
};

const Col = ({ cell }) => {
  useEffect(() => {
    console.log("Col MOUNTED", { cell });
  }, []);
  return <div>Col: {cell}</div>;
};

export default function App() {
  const col = <Col cell={<Cell cellData="hello world!" />} />;

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>

      <div>{col}</div>
    </div>
  );
}

enter image description here

enter image description here

You can see that the child component Cell is rendered first before the parent component Col logs that it is mounted.

Cell MOUNTED 
--START-- 
{cellData: "hello world!"}
--END-- 
Col MOUNTED 
{cell: Object}

Edit what-is-the-evaluation-order-for-jsx-in-react

Part of rendering a component is fully rendering out its subtree of children components. All children need to be rendered first. It is impossible for it to work any other way or in any other order. If React didn't have this deterministic order then it'd be impossible to diff the rendered DOM result from one render cycle to the next.

Just to throw out a simple answer to this:

Seems to run the code in Col first. But if props is just a function parameter, shouldn't it run the part first that was passed in?

The answer is no.

Consider this: React has no way of knowing what Col is doing with the cell prop. What if Col's implementation looked like this:

const Col = ({cell}) => {
   const [canRender,setCanRender] = useState(false);
   useEffect(() => {
     const timer = setTimeout(() => setCanRender(true),1000:
     return () => clearTimeout(timer);
   },[]);

   return <div>{canRender ? cell : null}</div>
}

React would waste time and effort calling Cell when Col isn't even going to render it immediately (or possibly ever).

Just to put a real fine point on it for the OP:

React only evaluates (e.g. calls) a component once it has been returned from it's immediate parent's render.

Related