Skip to content

Latest commit

 

History

History
163 lines (132 loc) · 5.56 KB

File metadata and controls

163 lines (132 loc) · 5.56 KB

This lesson is focused on memoization of function components. If you need to memoize a class-based component you can define the shouldComponentUpdate(nextProps, nextState, nextContext) method in your class, see https://react.dev/reference/react/Component#shouldcomponentupdate for details.

Memoization of components can be used to avoid unnecessary renders by not re-rendering the component if the props haven't changed. This is most useful when the component itself is expensive to render (e.g. the MathJax component in webapp) or it renders a lot of descedent components.

Memoization works by saving the rendered output of the component based on the props that are being passed in. Often times props will appear to have changed when their actual values haven't. In JavaScript, two objects with the same properties are considered different objects. Similarly, two functions with the same implementation are considered differen objects.

React.memo(Component, arePropsEqual?) is used to memoize components. If arePropsEqual isn't provided it does a shallow comparison of props and as such isn't able to determine when a prop that's an object or function are equal or not. In order for memoization to have the maximum benefit we need some way to determine if objects have the same contents and if callback props haven't changed.

We can pass a custom arePropsEqual function to override that behavior. To keep things simple we use a third-party library called react-fast-compare which provides a function that does a deep comparison of objects.

import arePropsEqual from "react-fast-compare";

type Props = {
    user: {name: string, id: string},
    onClick: () => void,
}

const ChildComponent = (props: Props) => {
    // ...
}

export default React.memo(ChildComponent, arePropsEqual);

There is a bit of a gotcha here when it comes to props that are functions. react-fast-compare cannot check if two functions are the same. Imagine the following scenario:

import ChildComponent from "./child-component";

type Props = {
    user: {name: string, id: string},
};

const ParentComponent = (props: Props) => {
    const result = useQuery(QUERY);

    const handleClick = () => {
        if (result.data) {
            // do something with the data
        }
    };

    return <ChildComponent user={props.user} onClick={handleClick}>
}

Each time ParentComponent renders, a new copy of handleClick will be created even if the result from useQuery isn't ready yet. We only want this function to treated as a new function when result changes. React provides a hook called useCallback which does exactly that by memoizing the function.

import ChildComponent from "./my-component";

type Props = {
    user: {name: string, id: string},
};

const ParentComponent = (props: Props) => {
    const result = useQuery(QUERY);

    const handleClick = React.useCallback(() => {
        if (result.data) {
            // do something with the data
        }
    }, [result]);

    return <ChildComponent user={user} onClick={handleClick}>
}

Parent is a class-based component

If the ParentComponent is a class-based component, functions passed as props to child components should be pre-bound methods. A pre-bound method never changes for the lifetime of component instance. If the prop is an inline function though, e.g. onClick={() => { ... }} a new version of the function will be created each time the component is rendered. Inline functions being passed as props should be converted to pre-bound methods as shown below.

BEFORE

class ParentComponent extends React.Component<Props, State> {
    componentDidMount() {
        fetch(QUERY).then((result) => {
            this.setState({result});
        });
    }

    render() {
        return <ChildComponent 
            user={user}
            // A new function will be pass to `onClick` each time
            // `ParentComponent` is rendered.
            onClick={() => {
                if (this.result?.data) {
                    // do something with the data
                }
            }}
        />
    }
}

AFTER

class ParentComponent extends React.Component<Props, State> {
    componentDidMount() {
        fetch(QUERY).then((result) => {
            this.setState({result});
        });
    }

    // pre-bound method
    handleClick = () => {
        if (this.result?.data) {
            // do something with the data
        }
    }

    render() {
        return <ChildComponent 
            user={user}
            onClick={this.handleClick}
        />
    }
}

WARNING: Memoization is not free. It requires memory so you should be picky when deciding what to memoize.

Good Candidates Bad Candidates
lots of descendants few descendants
expensive to render inexpensive to render
actual values of props change infrequently actual values of props change frequently

Exercise

  1. Use the profiler in React dev tools to measure the render performance of the code in the "exercise" folder.
  2. Update the code in the "exercise" folder memoize the Child component to avoid rerender this component.
  3. Use the profiler in React dev tools to measure the render performance again