Skip to content

Latest commit

 

History

History
172 lines (133 loc) · 4.62 KB

File metadata and controls

172 lines (133 loc) · 4.62 KB

Large components often end up re-rendering more stuff than is necessary when something changes in the component. If a component uses a lot of:

  • props
  • state
  • context usage
  • HOCs
  • hooks

a change to any of these things will result in the entire component re-rendering.

Splitting up a large component into a parent component and several smaller child components means that there will be fewer things (state, context usage, etc.) in the parent component that can trigger a re-render. It also means that those things will be spread out across multiple children so when one of them changes only one of the children will re-render.

Additionally, we can memoize some or all of the new child components if we want to prevent them from re-rendering in response to props changes.

Example

Consider LargeComponent below. Clicking on either of the buttons will cause the whole component to re-render.

import {useState} from "react";

import {expensiveComputation} from "./expensive-computation";

const LargeComponent = () => {
    const [foo, setFoo] = useState<number>(0);
    const [bar, setBar] = useState<number>(0);

    const renderFoo = () => {
        expensiveComputation();
        return <>
            <h1>foo = {foo}</h1>
            <button onClick={incrementFoo}>increment foo</button>
        </>;
    };

    const renderBar = () => {
        expensiveComputation();
        return <>
            <h1>bar = {bar}</h1>
            <button onClick={incrementBar}>increment bar</button>
        </>;
    };

    return <>
        <h1>LargeComponent</h1>
        {renderFoo()}
        {renderBar()}
    </>
}

Extracting components Foo and Bar allows those components to update and re-render indepedently of each other.

// foo.tsx
import {useState} from "react";
import {expensiveComputation} from "./expensive-computation";

export default function Foo() {
    const [foo, setFoo] = useState<number>(0);

    expensiveComputation();

    return = <>
        <h1>foo = {foo}</h1>
        <button onClick={incrementFoo}>increment foo</button>
    </>;
}

// bar.tsx
import {useState} from "react";
import {expensiveComputation} from "./expensive-computation";

export default function Bar() {
    const [bar, setBar] = useState<number>(0);

    expensiveComputation();

    return = <>
        <h1>bar = {bar}</h1>
        <button onClick={incrementBar}>increment bar</button>
    </>;
}

// parent.tsx
import Foo from "./foo";
import Bar from "./bar";

const ParentComponent = () => {
    return <>
        <h1>ParentComponent</h1>
        <Foo />
        <Bar />
    </Foo>
}

Notes

  • If there are still other things left in the ParentComponent that could trigger a re-render, you may want to memoize some or all of the children components.
  • Splitting up components has the added benefit of making the components easier to test. You can even mock the child components when testing the parent component.

Exercise

  1. Use the profiler in React dev tools to measure the render performance of the code in the "exercise" folder.
  2. Split LargeComponent in the exercise/ folder into a Parent component and multiple child components.
  3. Memoize components as necessary to address remaining render performance issues.
  4. Use the profiler in React dev tools to measure the render performance again.

Bonus - Mocking child components

The following pattern can be quite useful when writing jest tests for large components that render a number of subcomponents. The subcomponents can be mocked with module mocks to return simple strings that are easy to find using @testing-library/react's screen.findByText().

import {render, screen} from "@testing-library/react";
import {Parent} from "./parent";

jest.mock("./foo", ({
    __esModule: true,
    default: () => "Foo",
});

describe("Parent", () => {
    it("should render 'Foo'", async() => {
        // Arrange

        // Act
        render(<Parent />);

        // Assert
        const foo = await screen.findByText("Foo");
        expect(foo).toBeInTheDocument();
    });
});

If you want to verify that certain props were passed to a component you can use this pattern:

import {render, screen} from "@testing-library/react";
import {Parent} from "./parent";
import * as Bar from "./bar";

describe("Parent", () => {
    it("should render the child component", () => {
        // Arrange
        const barSpy = jest.spyOn(Bar, "default").mockReturnValue(<div />);

        // Act
        render(<Parent />);

        // Assert
        expect(barSpy).toHaveBeenCalledWith({msg: "hello, world!"}, {});
    });
});

NOTE: The extra {} in the call to toHaveBeenCalledWith is necessary.