React

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.7/underscore-min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gridstack@10.3.1/dist/gridstack.min.css">
<script src="https://cdn.jsdelivr.net/npm/gridstack@10.3.1/dist/gridstack-all.js"></script>
<div id="root"></div>

CSS

body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

li {
  margin: 8px 0;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}

.done {
  color: rgba(0, 0, 0, 0.3);
  text-decoration: line-through;
}

input {
  margin-right: 5px;
}

.grid-stack-item-content {
  background: #18bc9c;
  display: flex;
  align-content: center;
  justify-content: center;
  text-align: center;
}

React

function MyGridThing({
	items, // Map[ID, { x, y, w, h }]
	onChangeItems, // (update: Endo[Map[ID, { x, y, w, h }]]) => void
	children, // ID => JSX
}) {
	// Map[Id, Ref<Elem>]
	const itemRefs = React.useMemo(() => _.mapObject(items, () => React.createRef()), [items])

	// keep a Ref around the latest `onChangeItems` function
	const onChangeItemsRef = React.useRef(onChangeItems)
	onChangeItemsRef.current = onChangeItems

	// sync the `items` to a GridStack that we keep in `gridRef`
	const gridRef = React.useRef()
	const gridContainerRef = React.useRef()
	React.useEffect(() => {
		console.log('in MyGridThing useEffect to sync items to grid')
		const grid =
			gridRef.current ||
			(gridRef.current = GridStack.init(
				{ float: false, animate: false, margin: 5 },
				gridContainerRef.current,
			).on('change', (e, items) => {
				console.log('onChange event!')
				onChangeAdapter(onChangeItemsRef.current, items)
			}))
		grid.batchUpdate()
		grid.removeAll(false)
		for (const id in items) {
			const elem = itemRefs[id].current
			if (elem) {
				grid.makeWidget(elem, { id, ...items[id] })
			} else {
				console.info('missing ref for item', id)
			}
		}
		grid.batchUpdate(false)
		grid.setAnimation(true)
	}, [items, itemRefs])

	return (
		<div className="grid-stack controlled" ref={gridContainerRef}>
			{_.map(items, (item, id) => {
				return (
					<div ref={itemRefs[id]} key={id} className="grid-stack-item">
						<div className="grid-stack-item-content">{children(item, id, items)}</div>
					</div>
				)
			})}
		</div>
	)
}

function onChangeAdapter(onChangeReceiver, changedItems) {
	onChangeReceiver(oldItems => {
		const newItems = { ...oldItems }
		changedItems.forEach(({ id, x, y, w, h }) => {
			const oldItem = oldItems[id] || {}
			let changed = false
			if (oldItem.x != x || oldItem.y != y) {
				changed = true
				console.log('item', id, 'moved to', { x, y })
			}
			if (oldItem.w != w || oldItem.h != h) {
				changed = true
				console.log('item',...