2
\$\begingroup\$

I am an experienced developer, but new to the react and the javascript world. I am trying to create a resizeable sidebar in react(Please see the full code and demo here) for a personal project. Although it seems to be working I am not sure if this is correct. Here is the main part of the code.

I would like some feedback on:

  1. If this code is idiomatically correct? Like the use of useEffect, useState, and useRef.
  2. I am not sure how the resizeSidebar function is correctly resizing despite the asynchronous nature of setSidebarWidth
import React from "react";
import { useState, useEffect, useRef } from "react";
import "./App.css";

function App() {
  const sidebarRef = useRef(null);
  const resizerRef = useRef(null);
  const prevX = useRef(null);
  const [sidebarWidth, setSidebarWidth] = useState(268); 
  const [isTracking, setIsTracking] = useState(false);

  const resizeSidebar = (mouseMoveEvent) => {
    if (prevX && prevX.current) {
      let delta = mouseMoveEvent.clientX - prevX.current;
      setSidebarWidth(sidebarRef.current.getBoundingClientRect().width + delta);
      prevX.current = mouseMoveEvent.clientX;
    }
  };

  const stopResize = (event) => {
    window.removeEventListener("mousemove", resizeSidebar);
    window.removeEventListener("mouseup", stopResize);
    setIsTracking(false);
  };

  const startResizing = (mouseDownEvent) => {
    prevX.current = mouseDownEvent.clientX;

    window.addEventListener("mousemove", resizeSidebar);
    window.addEventListener("mouseup", stopResize);
  };

  useEffect(() => {
    if (!isTracking) prevX.current = null;
  }, [isTracking]);

  /*useEffect(() => {
      
  }, [sidebarWidth])*/

  useEffect(() => {
    const resizer = resizerRef.current;
    resizer.addEventListener("mousedown", startResizing);
    setIsTracking(true);
  }, []);

  return (
    <div className="app-container">
      <div
        ref={sidebarRef}
        className="app-sidebar"
        style={{ width: sidebarWidth }}
      >
        <div className="app-sidebar-content"></div>
        <div ref={resizerRef} className="app-sidebar-resizer"></div>
      </div>
      <div className="app-frame"></div>
    </div>
  );
}

export default App;

Here is a link to sandbox: https://codesandbox.io/s/react-resizable-sidebar-kz9de

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Your questions:

  1. Yes, but you need to add some useCallbacks. See below
  2. AFAIK, react renders instantly when you set state (for now), but you shouldn't rely on it. For one thing, multiple setState's will be batched together if in the same click listener (and even more places in react 18). Also, with the upcoming concurrent mode we'll get partial renders which will complicate things.

It's a bit buggy:

  • Moving the mouse to the right moves the line faster than the mouse. The opposite to the left. It happens when box-sizing is set to content-box, as the getBoundingClientRect().width accounts for borders and padding, while the width you set with content-box does not. Can be fixed by explicitly setting box-sizing to border-box, or as suggested below.
  • Sometimes I can't resize. It seems to try to drag the parent div, which disables the mousemove event. Fixed by adding an onMouseDown on the parent (app-sidebar) and e.preventDefault()

Code issues

  • useEffect has a dependency on startResizing which isn't listed. You should get an eslint warning about this.
  • since startResizing should be used as dependency to useEffect, you must wrap it with useCallback to avoid creating a new useEffect every time. The same goes for stopResize and resizeSidebar
  • isTracking seems to not be used. You can just set prevX.current to null instead of tracking false. You can remove the useEffect as well.
  • you can get rid of prevX entirely and calculate width with setSidebarWidth(mouseMoveEvent.clientX - sidebarRef.current.getBoundingClientRect().left) (this bypasses the content-box issue)
  • You don't need the resizeRef to add the mousedown listener. Just add onMouseDown directly to the element
  • With global listeners it's a good idea to put them into a useEffect with a cleanup function. That way you can be sure that everything is cleaned up. It would also help convince me that you don't have multiple event listeners active at the same time. Seems to work out ok in this example though.

Nitpicks

  • Delete commented code
  • div's with no content don't need an end tag. Close them with />
  • inside resizeSidebar you can use const for delta

Here's some of these ideas:

const sidebarRef = useRef(null)
const [isResizing, setIsResizing] = useState(false)
const [sidebarWidth, setSidebarWidth] = useState(268)

const startResizing = React.useCallback((mouseDownEvent) => {
    setIsResizing(true)
}, [])

const stopResizing = React.useCallback(() => {
    setIsResizing(false)
}, [])

const resize = React.useCallback((mouseMoveEvent) => {
    if (isResizing) {
        setSidebarWidth(mouseMoveEvent.clientX - sidebarRef.current.getBoundingClientRect().left)
    }
}, [isResizing])

React.useEffect(() => {
    window.addEventListener("mousemove", resize)
    window.addEventListener("mouseup", stopResizing)
    return () => {
        window.removeEventListener("mousemove", resize)
        window.removeEventListener("mouseup", stopResizing)
    }
}, [resize, stopResizing])

return (
    <div className="app-container">
        <div
            ref={sidebarRef}
            className="app-sidebar"
            style={{ width: sidebarWidth }}
            onMouseDown={e => e.preventDefault()}
        >
            <div className="app-sidebar-content" />
            <div
                className="app-sidebar-resizer"
                onMouseDown={startResizing}
            />
        </div>
        <div className="app-frame" />
    </div>
)

You could also replace isResizing with a ref to avoid running the effect everytime the user starts/stops resizing, but doesn't matter much.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.