Search code examples
reactjsrenderingsingle-page-application

Updating Parents state from Child without triggering a rerender of Child in React


So I'm trying to build a single page app in react.

What I want: On the page you can visit different pages like normal. On one page (index) i want a button the user can click that expands another component into view with a form. This component or form should be visible on all pages once expanded.

The Problem: The index page loads some data from an api, so when the index component gets mounted, an fetch call is made. But when the user clicks the "Expand form"-Button, the state of the Parent component gets updated as expected, but the children get rerendered which causes the index component to fetch data again, which is not what I want.

What I tried

// Parent Component
const App => props => {
    const [composer, setComposer] = useState({
        // ...
        expanded: false,
    });

    const expandComposer = event => {
        event.preventDefault();
        setComposer({
            ...composer,
            expanded: true
    });

    return(
        // ...
        <Switch>
            // ...
            <Route
                exact path={'/'}
                component={() => (<Index onButtonClick={expandComposer}/>)}
        // ....
        {composer.expanded && (
            <Composer/>
        )};
    );
};

// Index Component
const Index=> props => {
    const [isLoading, setIsLoading] = useState(true);
    const [data, setData] = useState([]);

    useEffect(()=> {
        // load some data
    }, []);

    if(isLoading) {
        // show spinner
    } else {
        return (
            // ...
            <button onClick={props.onButtonClick}>Expand Composer</button>
            // ...
        );
    };
};

So with my approach, when the button is clicked, the Index component fetched the data again and the spinner is visible for a short time. But I dont want to remount Index, or at least reload the data if possible


Solution

  • Two problems here. First, React will by default re render all child components when the parent gets updated. To avoid this behavior you should explicitly define when a component should update. In class based components PureComponent or shouldComponentUpdate are the way to go, and in functional components React.memo is the equivalent to PureComponent. A PureComponent will only update when one of it's props change. So you could implement it like this:

    const Index = () =>{/**/}
    export default React.memo(Index)
    

    But this won't solve your problem because of the second issue. PureComponent and React.memo perform a shallow comparison in props, and you are passing an inline function as a prop which will return false in every shallow comparison cause a new instance of the function is created every render.

    <Child onClick={() => this.onClick('some param')} />
    

    This will actually create a new function every render, causing the comparison to always return false. A workaround this is to pass the parameters as a second prop, like this

    <Child onClick={this.onClick} param='some param' />
    

    And inside Child

    <button onClick={() => props.onClick(props.param)} />
    

    Now you're not creating any functions on render, just passing a reference of this.onClick to your child.