๐ฅ 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
How it works
User cursor tracking with renderCallback
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
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-dateThe renderCallback reads userCursorsRef.current at call time, so it always sees the
current cursor positions without needing to re-create the policy object.
Simulated user actions
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.
Policy for the Priority column
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.
- Security: Implement proper authentication and authorization for collaborative features
- Scalability: Design for multiple concurrent users and large datasets
๐ฏ Common Use Cases
- 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
๐ Advanced Features
- 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
๐ Collaboration Patterns
- 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