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