DEV Community

pamlesleylu
pamlesleylu

Posted on

memo and useCallback() to the rescue

Since I use Angular at my day job and I really want to practice React, I thought of creating a simple application that will allow me to get my hands dirty. For this app, I needed a vertical resizer that will change the width sizes of the panels that are adjacent to it.
image

First, I implemented a Resizer component that renders a small vertical bar that users can click and drag left or right. This Resizer component listens to mouse events to capture the user's mouse movements.

import React, { useEffect } from 'react';

const Resizer = ({ onResize }: { onResize: (pageX: number) => void }) => {
  let dragging = false;

  const dragStart = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    event.preventDefault();
    dragging = true;
  };

  const dragMove = (event: MouseEvent) => {
    if (dragging) {
      onResize(event.pageX);
    }
  };

  const dragEnd = (event: MouseEvent) => {
    dragging = false;
  };

  useEffect(() => {
    window.addEventListener('mousemove', dragMove);
    window.addEventListener('mouseup', dragEnd);

    return () => {
      window.removeEventListener('mousemove', dragMove);
      window.removeEventListener('mouseup', dragEnd);
    };
  });

  return <div className="resizer" onMouseDown={dragStart}></div>;
};

export default Resizer;
Enter fullscreen mode Exit fullscreen mode

I then added an onResize event handler in the parent container to listen to the resize event emitted by the Resizer component. The handler just logs the new width received from the Resizer component to the console.

const App = () => {

  const onResize = (resizedWidth: number) => {
    console.log(resizedWidth);
    setNewWidth(resizedWidth);
  };

  return (
    <div>
      <div className="left-panel">
        LEFT
      </div>
      <Resizer onResize={onResize}></Resizer>
      <div className="right-panel">
        RIGHT
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

It worked perfectly.
image

So the next step was to adjust the width of the left panel when the onResize event gets emitted. To do this, I added newWidth state to the parent container and set the left panel's width to the value held by newWidth.

const App = () => {
  const [newWidth, setNewWidth] = useState(300);

  const onResize = (resizedWidth: number) => {
    console.log(resizedWidth);
    setNewWidth(resizedWidth);
  };

  return (
    <div>
      <div className="left-panel" style={{ width: newWidth }}>
        LEFT
      </div>
      <Resizer onResize={onResize}></Resizer>
      <div className="right-panel">
        RIGHT
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

But when I tried this out, the panels were not resizing and the developer console is only logging a single value.

image

UH-OH! :(

After looking into this a bit further, I figured out that the Resizer component gets re-rendered when the state changes in the parent container (i.e., when setNewWidth(resizedWidth); is called).

To fix this, I need to somehow make the Resizer not dependent on the parent container's state. Luckily, React has an API for this--the React.memo API. According to the documentation, React.memo is a higher order component that only checks for prop changes.

To make this work, I have to make sure that the props passed to the Resizer component does not change. To do this, I have to wrap the onResize event handler (props passed to Resizer) with useCallback.

const MemoizedResizer = memo<typeof Resizer>(({ onResize }) => (
  <Resizer onResize={onResize}></Resizer>
));

const App = () => {
  const [newWidth, setNewWidth] = useState(300);

  const onResize = useCallback((resizedWidth: number) => {
    console.log(resizedWidth);
    setNewWidth(resizedWidth);
  }, []);

  return (
    <div>
      <div className="left-panel" style={{ width: newWidth }}>
        LEFT
      </div>
      <MemoizedResizer onResize={onResize}></MemoizedResizer>
      <div className="right-panel">
        RIGHT
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

After application of the said fixes... VOILA!

resizer

Top comments (0)