How to update values in function passed to a child component as props

Viewed 24

Objective

I have a container component that can have several child components that are selectors rectangles (they work similarly to a click+drag when selecting files for example). These child components that I call "Selectors" are rendered by the container and can be resized after their creation.

Context

The container holds a state "selectorGroup" which is an array of all the selectors created and their values for top, left, width and height in the format:

{ id, top, left, width, height, containerCoords, onResize }

I get the updated values for top, left, width and height within the resized Selectors and pass it to the onResize prop function which stays in the parent component:

const updateSelector = useCallback(
    (updated) => {
      selectorRef.current = updated;
      setSelectorGroup((group) => {
        const old = group.filter((selec) => selec.id === updated.id);
        const selector = { ...old, ...updated };
        const updatedGroup = group.splice(selector.id, 1, selector);
        return [group];
      });
    },
    [selectorGroup]
  );

The problem with that

When a Selector is created it receives a function "updateSelector" as a prop called onResize. This function uses the selectorGroup state of existing selectors from the container component, but if this state is updated as when a new Selector is created the Selectors won't recieve the array with updated data. In fact they keep the received array from the state when they were created.

What I'm looking for

I need a method to pass a function as a prop to the child components and have them updated whenever a parent's state is updated. Right now the function gets updated (as expected) but not for those children that got it before the state update, only for any new child component created.

I've read about forwardRef() and useImperativeHandle() but these don't seem to fit in my case.

Here is my code

Parent Component

export default const Parent = forwardRef(({ withLines, children }, ref) => {
  const containerRef = useRef();
  const selectorRef = useRef();
  const [selectorGroup, setSelectorGroup] = useState([]);
  const [containerCoords, setContainerCoords] = useState({});

  const updateSelector = useCallback(
    (updated) => {
      console.log(updated);
      selectorRef.current = updated;
      // setSelectorGroup((group) => {
      //   const old = group.filter((selec) => selec.id === updated.id);
      //   const selector = { ...old, ...updated };
      //   console.log(selector);
      //   const updatedGroup = group.splice(selector.id, 1, selector);
      // return [group];
      // });
    },
    [selectorGroup]
  );

  const handleContainerMouseDown = useCallback(
    (e) => {
      e.preventDefault();

      const { ctrlKey } = e;

      if (ctrlKey) {
        const top = Number(e.clientY - containerCoords.top);
        const left = Number(e.clientX - containerCoords.left);

        selectorRef.current = {
          id: `${selectorGroup.length ? selectorGroup.length : '0'}`,
          top,
          left,
          width: 50,
          height: 50,
          lines: [],
          containerCoords,
          onResize: updateSelector,
        };

        setSelectorGroup((group) => [...group, selectorRef.current]);
      }
    },
    [containerCoords, selectorGroup, updateSelector]
  );

  /** Load container coords */
  useEffect(() => {
    setContainerCoords(containerRef.current.getBoundingClientRect());
  }, [containerRef]);

  return (
    <Container ref={containerRef} onMouseDown={handleContainerMouseDown}>
      {selectorGroup.map((selector, index) => (
        <Child key={index} {...selector} />
      ))}
      {children}
    </Container>
  );
});

Child Component

export default function Child({
  id,
  top,
  left,
  width,
  height,
  onResize,
  containerCoords,
}) {
  const resizerRef = useRef();
  const [container, setContainer] = useState({});

  const handleResize = useCallback((e) => {
    if (resizerRef && resizerRef.current) {
      const { id: resizer } = resizerRef.current;
      const { id: selector } = resizerRef.current.parentElement.parentElement;
      const selectorCoords = resizerRef.current.parentElement.parentElement.getBoundingClientRect();

      const mouseX = e.pageX - containerCoords.left;
      const vectorX = mouseX - selectorCoords.left;

      const newLeft = selectorCoords.left + vectorX;

      onResize({
        id: selector,
        top: selectorCoords.top,
        left: newLeft,
        width: 50,
        height: 50,
      });
    }
  }, []);

  const handleMouseMove = useCallback((e) => {
    handleResize(e);
  }, []);

  const handleMouseUp = useCallback(() => {
    if (resizerRef && resizerRef.current) {
      resizerRef.current.removeEventListener('mousemove', handleMouseMove);
      resizerRef.current = null;
    }
  }, [resizerRef, handleMouseMove]);

  const handleMouseDown = useCallback(
    (e) => {
      resizerRef.current = e.target;
      resizerRef.current.addEventListener('mousemove', handleMouseMove);
    },
    [resizerRef, handleMouseMove]
  );

  useEffect(() => {
    setContainer(containerCoords);
    window.addEventListener('mouseup', handleMouseUp);
  }, []);

  return (
    <Wrapper
      id={id}
      className="selector"
      top={top}
      left={left}
      width={width}
      height={height}
    >
      <Resizers>
        <Resizer
          id="top-left"
          className="resize"
          onMouseDown={handleMouseDown}
        />
        <Resizer
          id="top-right"
          className="resize"
          onMouseDown={handleMouseDown}
        />
        <Resizer
          id="bottom-left"
          className="resize"
          onMouseDown={handleMouseDown}
        />
        <Resizer
          id="bottom-right"
          className="resize"
          onMouseDown={handleMouseDown}
        />
      </Resizers>
    </Wrapper>
  );
}

What they look like: example image

0 Answers
Related