Skip to content

Case 7: Real-time Collaboration Interface

This demo simulates multiple users working on the same spreadsheet simultaneously, with user avatars shown directly inside cells and a live activity feed.

📝 Activity Feed

View Source Code

User avatars are displayed inside cells using renderCallback on the default policy. This approach is scroll-safe because the callback runs inside each cell’s DOM node — no absolute positioning relative to the grid container is needed.

const policies = React.useMemo(() => ({
default: new Policy({
mixins: [{
renderCallback(rendered, { point }) {
const cursorsHere = Object.entries(userCursorsRef.current).filter(
([, pos]) => pos.row === point.y && pos.col === point.x,
);
if (cursorsHere.length === 0) return rendered;
const users = cursorsHere
.map(([id]) => virtualUsers.find((u) => u.id === id))
.filter(Boolean);
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
{rendered}
<div style={{ position: 'absolute', top: 2, right: 2, display: 'flex', gap: '2px' }}>
{users.map((user) => (
<span key={user.id} title={user.name}>{user.avatar}</span>
))}
</div>
</div>
);
},
}],
}),
priority: new Policy({ mixins: [PriorityPolicy] }),
}), []);

renderCallback receives the already-rendered cell content as rendered and wraps it. By returning a new React element, you can overlay any UI on top of the default rendering without replacing it.

Why userCursorsRef instead of reading state directly

Section titled “Why userCursorsRef instead of reading state directly”

The useMemo callback is evaluated only once ([] deps), so the policy closure would capture a stale copy of userCursors state if used directly. Instead, a ref is kept in sync with the latest state on every render:

const userCursorsRef = React.useRef({});
// ...
userCursorsRef.current = userCursors; // always up-to-date

The renderCallback reads userCursorsRef.current at call time, so it always sees the current cursor positions without needing to re-create the policy object.

A setInterval fires every 1.2 seconds and picks a random virtual user, moves their cursor to a random cell, and (with higher probability) actually edits that cell via sheetRef:

apply(sheet.update({
diff: { [cellAddress]: { value: newValue } },
}));

The activity log keeps the last 10 actions and is displayed in the sidebar.

The priority policy is assigned to the range E1:E5 in initialCells. It currently uses an empty mixin (PriorityPolicy = {}), but you can add custom render* methods or validation logic there to extend the behaviour of that column independently.

  1. Security: Implement proper authentication and authorization for collaborative features
  2. Scalability: Design for multiple concurrent users and large datasets
  • Team Collaboration: Real-time team spreadsheet editing
  • Project Management: Collaborative project tracking and planning
  • Financial Modeling: Multi-user financial analysis and modeling
  • Data Entry: Collaborative data entry and validation
  • Documentation: Real-time collaborative documentation creation
  • Real-time Synchronization: Live data synchronization across users
  • Conflict Resolution: Handle concurrent editing conflicts
  • User Presence: Show who is currently viewing or editing
  • Change History: Track all changes with user attribution
  • Offline Support: Work offline with sync when reconnected
  • Operational Transformation: Handle concurrent edits with operational transformation
  • Conflict-free Replicated Data Types: Use CRDTs for conflict resolution
  • Event Sourcing: Track all changes as events for audit and replay
  • State Synchronization: Efficient state synchronization across clients
  • User Session Management: Manage user sessions and authentication