React's render() Doesn't... Render

Adam Nathaniel Davis - Apr 25 '20 - - Dev Community

My recent foray into functional components has made me realize that there's a lot of confusion out there about the React rendering cycle. I'm not pointing a general finger at anyone else. I'm raising my hand and acknowledging my own misconceptions. I've been doing React development now for years, but I'm still finding places where my knowledge has been... lacking.

Naming Things Is Hard

React devs talk a lot about rendering and the rendering cycle and, if you're looking at a class component, the render() function. The problem with these terms is that they all imply an action. Specifically, they imply that something will, well... you know... be rendered. But that's not necessarily the case at all. And misunderstanding the distinctions can be detrimental to our work.

This might be one area where the naming convention embedded in class components is, if not harmful, at least, obtuse. I say this because every single class component in React must have a render() function defined. If there is no render() function, the class won't compile as a React component.

Maybe that doesn't strike you as a problem, but think for a moment about how we typically name our functions. And think about what is typically implied by those names. To illustrate this, take a look at these actual function names that are drawn from one of my React projects:

const deleteUser = (userId = '') => { 
  // function logic here 
};

const getRows = () => { 
  // function logic here 
};

const sortUsers = (column = '', direction = '') => { 
  // function logic here 
};
Enter fullscreen mode Exit fullscreen mode

You don't need to understand anything about my app to know what these functions do. The function names clearly tell you what happens when you call them.

But there's another truth that we can imply when we see functions like these. The understanding is typically that this functionality will do what the name implies it will do every single time we call that function, and only when we call that function.

In other words, we don't need to wonder "How many times will a user be deleted?" The answer is, "As many times as the deleteUser() function is called."

We don't need to worry about whether we are needlessly sorting-and-resorting the users. All we need to do is find anyplace in the app where sortUsers() is being called. Because the users will be sorted whenever sortUsers() is called, and only when sortUsers() is called.

Now let's look at something that we see in every single class component:

export default class Yo extends React.Component {
  render = () => {
    return <div>Yo!</div>;
  }
}
Enter fullscreen mode Exit fullscreen mode

As simple as this may look, it kinda breaks our universal, fundamental understanding of exactly how functions work. Don't believe me? Well, consider these points:

  1. Calling render() doesn't necessarily return anything. Inside the guts of React, I'm sure it's reasonable to state that the return statement is executed every single time render() is called. But from the perspective of someone who doesn't live inside the React engine, this function usually won't return anything at all. In fact, since the component is stateless and the content is static, the return statement really only returns anything once during its entire lifecycle, even though it may be called repeatedly.

  2. Which leads to my second point: Exactly how often will render() be called, anyway? Who the hell knows??? In a React application, it can be virtually impossible to know exactly when this render() will be called and how often it will be called. That's because it's tied to the component lifecycle. In a React application, you never call render() directly. And yet, render() gets called repeatedly, for every component, sometimes in use-cases that are hard to fully understand.

  3. Although this is somewhat semantic, "render" doesn't really describe what the render() function is actually doing. And I believe this accounts for at least some of the confusion. In my book, "render", in a web-based application, means something like, "I'm painting something on the screen." But there are many times that calling render() can result in no updates whatsoever being painted to the screen. So, from that perspective, it would probably have been clearer if the required render() function were, in fact, called something like, checkForRenderingUpdates(), or renderIfContentHasChanged(). Because that's much more akin to what it's actually doing.

Greater Clarity(???) With Functions

Does this get any "better" or "cleaner" if we switch to functional components? Umm... maybe?? Consider the functional equivalent:

export default function Yo() {
  return <div>Yo!</div>;
}
Enter fullscreen mode Exit fullscreen mode

On one hand, we've removed the ambiguity of that render() function because there is no render() function. On some level, that's "good".

But I've noticed that this doesn't do much to clarify developers' understanding of how React is checking for updates. In fact, it has the potential to further obfuscate the process because there simply is no built-in indication, inside the component definition, that spells out just how-or-when this component is being re-rendered.

This can be further muddied because functional components come with none of the traditional "lifecycle methods" that we had at our disposal in class components. You can say what you want about lifecycle methods - and sometimes they can be an absolute pain to deal with. But the only thing worse than managing component lifecycle with the lifecycle methods of class components, is trying to manage lifecycle processes in functional components - which have no lifecycle methods. And at least, when you had those lifecycle methods at your disposal, they served as a tangible marker of the component's native lifecycle.

This is where I sometimes find functional components to be more confusing, and more obtuse, than class components. I've already talked to a good number of functional-programming fanboys who stridently believe that: If a functional component is called, then it is also rendered. But this simply isn't true.

It is true that, every time you call a functional component, the rendering algorithm is invoked. But that's a far cry from saying that the component is rerendered.

Static Components

Let's look at where the rendering conundrum causes a lot of confusion:

export default function App() {
  const [counter, setCounter] = useState(0);
  return (
    <div>
      <button onClick={() => setCounter(counter + 1)}>Increment ({counter})</button>
      <Child/>
    </div>
  );
}

function Child() {
  console.log('Child has been called');
  return (
    <div>
      I am a static child.
      <Grandchild/>
    </div>
  );
}

function Grandchild() {
  console.log('Grandchild has been called');
  return (
    <div>I am a static grandchild.</div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We have three layers in our app:

<App><Child><Grandchild>

<App> is a stateful component. It holds and updates the counter value. <Child> and <Grandchild> are both pure components. In fact, they're both static components. They accept no input, and they always return the same output. Although they're both descendants of <App>, they have no dependencies upon <App>, or <App>'s counter variable - or upon anything else for that matter.

If you plopped <Child> or <Grandchild> into the middle of any other app, at any particular location, they'd do the exact same thing - every time.

So here's where it seems to me like there's still a lot of confusion out there. What happens when you click the "Increment" button?? Well, it goes like this:

  1. The counter state variable inside <App> gets updated.
  2. Because there has been a change to <App>'s state, <App> rerenders.
  3. When <App> rerenders, <Child> is called.
  4. <Child>, in turn, calls <Grandchild>.

But here's where things get sticky. The rerendering of <App> will result in <Child> being called. But does that mean that <Child> was rerendered??? And will calling <Child>, in turn, lead to <Grandchild> being rerendered???

The answer, in both cases, is: No. At least, not in the way that you might be thinking.

(BTW, I put the console.log()s in this example because this is exactly what I've seen other people do when they're trying to "track" when a given component is rendered. They throw these in, then they click the "Increment" button, and then they see that the console.log()s are triggered, and they say, "See. The entire app is being rerendered every time you click the 'Increment' button." But the console.log()s only confirm that the component is being called - not that it's being rendered.)

In this demo app, people often say that, "The entire app is being rerendered every time you click the Increment button." But at the risk of sounding like a "rules lawyer", I would reply with, "What exactly do you mean by 'rerendered'??"

Reconciling, Not Rerendering

According to the React documentation on Reconciliation, this is what's basically happening when a render() is invoked:

When you use React, at a single point in time you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.

(You can read the full documentation here: https://reactjs.org/docs/reconciliation.html)

Of course, the explanation above implies that there are differences in the before-and-after trees of React elements. If there are no differences, the diffing algorithm basically says, "do nothing".

For this reason, I almost wish that React's render() function was instead renamed to reconcile(). I believe that most devs think of "rendering" as being an active process of drawing/painting/displaying elements on a screen. But that's not what the render() method does. React's rendering cycle is more like this:

const render = (previousTree, currentTree) => {
  const diff = reconcile(previousTree, currentTree);
  if (!diff)
    return;
  applyDOMUpdates(diff);
}
Enter fullscreen mode Exit fullscreen mode

This is why it can be a misnomer to imply that a static component is ever truly "rerendered". The render process may be called on the static component, but that doesn't mean that the component will truly be "rerendered". Instead, what will happen is that the React engine will compare the previous tree with the current tree, it will see that there are no differences, and it will bail out of the render process.

DOM Manipulation Is Expensive, Diffing Is Not

You may see this as an inconsequential distinction. After all, whether we call it "rendering" or "reconciling", there is still some sort of comparison/computation being run every single time that we invoke the render cycle on a component. So does it really matter if the reconciliation process short circuits before any real DOM manipulation can be applied??

Yes. It matters. A lot.

We don't chase down unnecessary rerenders because our computers/browsers are so desperately constrained that they can't handle a few more CPU cycles of in-memory comparisons. We chase down unnecessary rerenders because the process of DOM manipulation is, even to this day, relatively bulky and inefficient. Browsers have come lightyears from where they were just a decade ago. But you can still drive an app to its knees by needlessly repainting UI elements in rapid succession.

Can you undermine an app's performance merely by doing in-memory comparisons of virtual DOM trees? I suppose it's technically possible. But it's extremely unlikely. Another way to think of my pseudo-code above is like this:

const render = (previousTree, currentTree) => {
  const diff = quickComparison(previousTree, currentTree);
  if (!diff)
    return;
  laboriousUpdate(diff);
}
Enter fullscreen mode Exit fullscreen mode

It's almost always an unnecessary micro-optimization to be focused on the quickComparison(). It's much more meaningful to worry about the laboriousUpdate().

But don't take my word for it. This is directly from the React docs, on the same page that explains the Reconciliation process (emphasis: mine):

It is important to remember that the reconciliation algorithm is an implementation detail. React could rerender the whole app on every action; the end result would be the same. Just to be clear, rerender in this context means calling render for all components, it doesn’t mean React will unmount and remount them. It will only apply the differences following the rules stated in the previous sections.

Conclusions

Obviously, I'm not trying to say that you shouldn't care about unnecessary rerenders. On some level, chasing them is part of the core definition of what it means to be a "React dev". But calling your components is not the same as rendering your components.

You should be wary of unnecessary rerenders. But you should be careful about the term "rerender". If your component is being called, but there are no updates made to the DOM, it's not really a "rerender". And it probably has no negative consequences on performances.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player