Skip to content

fix: stabilize selector-derived room snapshots - #13

Closed
under-undefined wants to merge 1 commit into
colyseus:mainfrom
under-undefined:fix/derived-root-snapshot-cache
Closed

under-undefined wants to merge 1 commit into
colyseus:mainfrom
under-undefined:fix/derived-root-snapshot-cache

Conversation

@under-undefined

@under-undefined under-undefined commented Aug 18, 2026 •

Copy link
Copy Markdown
## Summary

Fixes an infinite render loop in `useRoomState()` / `useColyseusState()` when a selector returns a newly derived root array or plain object.

For example:

```tsx
useRoomState(room, (state) => Array.from(state.players.values()));

useRoomState(room, (state) => ({
  message: state.myString,
}));

Problem

Colyseus Schema, MapSchema, and ArraySchema nodes have stable refId values, allowing createSnapshot() to reuse previous results through resultsByRefId.

Selector-created arrays and objects do not have a Colyseus refId. As a result, every getSnapshot() call produced a new root reference, even when the room state had not changed.

This violates the referential stability requirement of useSyncExternalStore() and causes React to repeatedly render until it throws:

The result of getSnapshot should be cached to avoid an infinite loop
Maximum update depth exceeded

Solution

Add hook-local structural sharing for selector-derived root containers that do not have a Colyseus refId.

The new behavior:

  • Reuses the previous root snapshot when its contents are unchanged.
  • Creates a new root snapshot when selected data changes.
  • Preserves references for unchanged nested values.
  • Keeps the existing resultsByRefId behavior unchanged for normal Colyseus Schema roots.
  • Keeps selectors operating on the live Colyseus state, preserving APIs such as MapSchema#get() and MapSchema#values().
  • Resets reuse boundaries when the room state or decoder changes.

Tests

Added regression coverage for:

  • A selector returning a derived root array.
  • A selector returning a derived plain object.
  • Updating the underlying room state after the derived snapshot has been cached.

@endel

endel commented Aug 18, 2026

Copy link
Copy Markdown
Member

Hi @under-undefined - thank you for reporting and for sharing the reproduction scenarios - can you please check if version 0.1.19 fixes this for you?

Took a slight different approach to your PR there

@under-undefined

Copy link
Copy Markdown
Author

@endel 0.1.19 Fixed 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants