The new components in town that replaced the class-based components are the functional components and their lifecycle can easily be confusing, especially together with memoized components. To understand what actually happens with renders, we are going to look at a bunch of examples that I’ve prepared and look at the reasons why it might be what it is.

TL; DR

  • Try and avoid inline functions in class components as much as possible as they get recreated on render.
  • In functional components, there is no difference between declaring a function outside of the return or declaring an inline function. Colocation is good but be wary of code readability.
  • If you can, declare your functions hoisted in functional components outside of your actual component, this avoids recreation of said function.

When Do Components Re-render?

The first thing we need to set in stone is when the components re-render as this is the core of this article. When a component’s prop or state changes, this component gets re-rendered. Additionally, if the parent of this component also has a re-render through these rules, even if mentioned parent is not passing any props to the child, the child component would get re-rendered.

Memoized Components

The exception for this would be memoized components, but you can think of these rules as valid for regular components that we love and use. The memoized components (PureComponent, React.memo) do not get re-rendered if the parent re-renders without changing any reference of the props. What does this mean? JS objects do not have the same reference even if they look exactly the same.

const first = [1, 2, 3];
const second = [1, 2, 3];

console.log(first === second); //false

This happens because the references of these two objects are not the same even though it would be the same for strings and integers. So, whenever the parent component is re-rendering, if for example an array like this is passed to a memo component as an inline prop, it would simply recreate this object, therefore causing the object to have a new reference. This means the prop of the memo component actually changed and then the re-render will be triggered.

Example

In this example project you would see how the renders affect the child components throughout the hierarchy. The project basically checks for renders through useEffect without any exhaustive deps, which basically causes the effect to fire every time there is a re-render. For the class component I’ve included the componentWillUpdate call, which basically does the same thing.

Don’t forget that functions are also objects!

It might be easier to open the project separately or even download it.

You can see that no matter what you do, if you do not explicitly re-render the memo component that is using the function from outside of its parent, it would not get re-rendered.

My React.memo Component Is Re-rendered Even Without an Inline Object!

This is a common misunderstanding that people who are very used to class components face. Class-based components used to work differently and they did not actually require recreating the functions inside.

Class Components

Classes by their nature and logic are declared once and are used with the same reference. React also tried and followed this logic with their class components. Let’s take a look at the class component from the example project:

import React, { Component } from "react";
import PureClassChild from "./PureClassChild";

let classRendered = 0;
export default class ClassChild extends Component {
  regularFunction() {
    console.log("I am a class function");
  }

  UNSAFE_componentWillUpdate() {
    classRendered++;
  }

  render() {
    return (
      <div>
        <p>{`Class child rendered ${classRendered} times`}</p>
        <PureClassChild regularFunction={this.regularFunction} />
      </div>
    );
  }
}

The only reason UNSAFE_componentWillUpdate was added here was so that I can easily display the render count, otherwise I definitely do not advise the usage of this function anymore as it is deprecated.

Beware the eyesore paragraph ahead!

So, inside this class component we have two important functions. regularFunction and render. Now to think about what happens here once this component gets re-rendered. Unlike functional components, when a class component gets re-rendered it will only call the render function again instead of going through the construction of the class all over again. This means that the regularFunction is actually not going to get recreated on every render and unless I call it with an arrow function like onClick={() => this.regularFunction()} or declare regularFunction right inside the render function, then the reference of this.regularFunction will stay the same.

I know that’s a complicated paragraph, but the most important part to take from it is that the this.regularFunction is not getting recreated on every render in this class component, which means the PureClassChild actually won’t be getting re-rendered.

You can easily test this inside the project by simply pressing the Change App State button. Then you would see that the class component itself is getting re-rendered but the pure component inside is actually not, because nothing about its props changed or got recreated during the re-render.

It’s Alive!

Let’s take a closer look at this class component, just so that we can see why this is happening. This is what our class component looks like after it’s been struck by Babel:

let p=0;
class b extends a.Component {
  regularFunction() {
    console.log("I am a class function");
  }
  UNSAFE_componentWillUpdate() {
    p++;
  }
  render() {
    return c.a.createElement(
      "div",
      null,
      c.a.createElement("p", null, "Class child rendered ".concat(p, " times")),
      c.a.createElement(E, { regularFunction: this.regularFunction })
    );
  }
}

The transliteration used here is with the default settings, there are no extra changes. As you can see it still actually looks pretty similar. If you are curious about how it is on React Native with module:metro-react-native-babel-preset it looks tad different.

[
  {
    key: "regularFunction",
    value: function () {
      console.log("I am a class function");
    },
  },
  {
    key: "render",
    value: function () {
      return (
        v++,
        s.default.createElement(
          y.View,
          { style: { margin: 12 } },
          s.default.createElement(
            y.Text,
            null,
            "Class child rendered " + v + " times"
          )
        )
      );
    },
  },
]

It’s all separated into objects and regularFunction is still only referenced and not recreated inside the render. You can see the example project on React Native with similar stuff inside here:

Now that we have seen what class components are made of, it’s time to move on to functional components.

I’m a Functioning Developer

Let’s focus on the Parent component that we have, as it is the component with the most variety of functions.

let parentRendered = 0;
function memoConstantFunction() {
  console.log("I'm a function");
}
parentRendered = 0;

export default function Parent() {
  const [memoChildProp, setMemoChildProp] = useState(0);
  const [forRender, setForRender] = useState(0);

  useEffect(() => {
    parentRendered++;
  });

  function memoDynamicFunction() {
    console.log("dynamic function");
  }

  return (
    <div>
      ...
      <MemoChild
        index={0}
        name="Memo With Inline Function"
        someObject={someObject}
        memoChildProp={memoChildProp}
        inlineFunctionExample={() => console.log("inlineFuncExample")}
      />
      ...
    </div>
  );
}

const someObject = { a: 1 };

As you can see here there are three types of functions we declared. Inline arrow function, a function inside the Parent component and one outside of the Parent component.

It is recommended that whenever you can, you should declare your functions outside of the component. The reason for this is, on every render functional components would run all the code it has inside of them, this is a contrast they have with class components. In this case the memoDynamicFunction and the function that has been given to the inlineFunctionExample prop would get redeclared when there is a re-render, but the memoConstantFunction would stay the same and have the same reference. This is important for React.memo components especially if you want to keep them working as they should.

First to Destroy It, Then Re-create It

Now for the Parent component after transliteration:

let h = 0;
function C() {
  console.log("I'm a function");
}
function f() {
  const e = Object(a.useState)(0),
    t = Object(r.a)(e, 2),
    n = t[0],
    l = t[1],
    o = Object(a.useState)(0),
    m = Object(r.a)(o, 2),
    u = m[0],
    d = m[1];
  return (
    Object(a.useEffect)(() => {
      h++;
    }),
    c.a.createElement(
      "div",
      null,
      c.a.createElement(
        "div",
        null,
        c.a.createElement(
          "button",
          { onClick: () => d(u + 1) },
          "Change Parent State"
        )
      ),
      c.a.createElement(
        "div",
        null,
        c.a.createElement(
          "button",
          { onClick: () => l(n + 1) },
          "Change MemoChild prop"
        )
      ),
      c.a.createElement(s, {
        index: 0,
        name: "Memo With Inline Function",
        someObject: O,
        memoChildProp: n,
        inlineFunctionExample: () => console.log("inlineFuncExample"),
      }),
      c.a.createElement(s, {
        index: 1,
        name: "Memo With Function Inside Parent",
        someObject: O,
        memoChildProp: n,
        memoDynamicFunction: function () {
          console.log("dynamic function");
        },
      }),
      c.a.createElement(s, {
        index: 2,
        name: "Memo With Inline Style",
        someObject: { a: 1 },
        memoChildProp: n,
        memoConstantFunction: C,
      }),
      c.a.createElement(s, {
        index: 3,
        name: "Memo With Function Outside Parent",
        someObject: O,
        memoChildProp: n,
        memoConstantFunction: C,
      }),
      c.a.createElement(i, null),
      c.a.createElement(b, null),
      c.a.createElement("p", null, "Parent rerendered ".concat(h, " times"))
    )
  );
}
h = 0;
const O = { a: 1 };

The memoConstantFunction got renamed to a simple C and is still outside of our component, so still globally defined which would save it from getting recreated. Our inline function on the other hand still remains as an inline function inside inlineFunctionExample: () => console.log("inlineFuncExample").

What’s the most interesting here is what happens to the memoDynamicFunction. It’s been moved inside the createElement function and declared like an inline function. What this means for us is basically that there is no visible difference between inline functions and functions declared inside a component.

Final Thoughts

It’s highly recommended that you declare your functions outside of the components if you can, but this is not always possible, especially if you are dealing with the state inside your function for example.

If you have to keep your function inside your component and your function is a one-liner like setState({...}), it’s not really a bad thing to keep it inside your return. Colocation is very welcome and if done well, makes the code more readable. This by no means is a push towards declaring everything as inline, the readability of the code is an important aspect in all this.