Why React child component is not rerendering when props change?

This morning I was trying to render a child component that was receiving props. The problem was that it didn't rerender after the props changed. 😱

Let's analyze what the hell was happening...

A simplified version of the parent was:

jsx
import react, { useState, useEffect }  from 'react';
import moment                          from 'moment';

const App = () => {
    
    const [time, setTime] = useState(moment());
    
    return(
        
        <Timer 
            time    = { time }
            setTime = { setTime }
        />
        
    );
    
}

export default App;

The child got the time from the parent and I could add minutes to it by clicking a button:

jsx
const Timer = ({ time, setTime }) => {
    
    const addMinutes = (mins) => {
        
        // Adding 15 minutes to the timer
        let newTime = time.add(15, 'minute');
        
        // Setting new state
        setTime(newTime);
        
    }
    
    return(
        <div className = 'Timer'>
            The time is {time.format('LT')}
            <button onClick = { () => addMinutes(15) }>Click</button>
        </div>
    )
    
}

The child is not rerendering. But... why?

After clicking the button of the timer, the text rendered remained equal to the one on the first render. 🧐

The problem was that I was adding 15 minutes to the exact same object received as a prop. Since I was modifying the same reference, React couldn't detect a state change. Hence, the child wasn't rerendering.

Let's refresh some JavaScript theory:

jsx
let object = {name: 'Erik', age: 29};

// Here we are not making a copy of the object
// Both objects have the same reference
let objA = object;
let objB = object;

// We are modifying the original object
objA.age = 99;

// Hence...
console.log(objA.age); // 99
console.log(objB.age); // 99

The solution is to clone the original object:

jsx
let object = {name: 'Erik', age: 29};

// We create a copy of the original object
// Using the spread (...) operator
let objA = {...object};
let objB = {...object};

// Now we aren't modifying the original object
objA.age = 99;

// Hence...
console.log(objA.age); // 99
console.log(objB.age); // 29

Solution

I needed to clone the original object received as a prop:

jsx
const Timer = ({ time, setTime }) => {
    
    const addMinutes = (mins) => {
        
        // ✅ Cloning the time object
        let clone   = moment(time);
        
        // Adding 15 minutes to the timer
        let newTime = clone.add(15, 'minute');
        
        // Setting new state
        setTime(newTime);
        
    }
    
    return(
        <div className = 'Timer'>
            The time is {time.format('LT')}
            <button onClick = { () => addMinutes(15) }>Click</button>
        </div>
    )
    
}

Now React was detecting that the previous state and the current one were different objects. Thus, the parent/child rerendered.

Hi, I'm Erik, an engineer from Barcelona. If you like the post or have any comments, say hi.