How to update a state when a prop changes, without rendering twice

Tags: , , ,



I have a data visualization component that has “width” as one of the props. A state keeps track of the zoom level in the chart, and it is updated when handling mouse events.

The zoom needs to adjust when the width changes. Currently, I’m doing this inside a useEffect hook:

function MyComponent({width}) {
    const [zoom, setZoom] = useState(...)
    
    useEffect(() => {
        setZoom(adjustZoom(zoom, width))
    }, [width])
    
    const handleMouseEvent = (event) => {
        setZoom(calculateNewZoom(event))
    }    
    
    ...
}

But this makes the component render twice: once for the width update, and once for the zoom update. Since the first render flashes on screen, it is not ideal.

Ideally, the component would render only once, reflecting both the changes in width and zoom at the same time. How to achieve this with hooks? Also, is there a name for this concept? Thanks in advance.

Answer

Since the first render flashes on screen, it is not ideal.

This is what useLayoutEffect() is intended to solve, as a drop-in replacement for useEffect().

You have another potential problem though, which is that your useEffect() contains a stale reference to zoom. In order to obtain the correct reference, use the functional update form of setZoom() instead:

function MyComponent({ width }) {
    const [zoom, setZoom] = useState(...)

    useLayoutEffect(() => {
        setZoom((zoom) => adjustZoom(zoom, width))
    }, [width])

    const handleMouseEvent = (event) => {
        setZoom(calculateNewZoom(event))
    }
    ...
}

Alternatively, you might consider dropping the useLayoutEffect() and using a memoized adjustedZoom instead to avoid double-rendering:

function MyComponent({ width }) {
    const [zoom, setZoom] = useState(...)

    const adjustedZoom = useMemo(() => {
        return adjustZoom(zoom, width)
    }, [zoom, width])

    const handleMouseEvent = (event) => {
        setZoom(calculateNewZoom(event))
    }
    ...
    // now use adjustedZoom where you would have used zoom before
}


Source: stackoverflow