An app to manage tasks within multiple lists, with realtime + offline synchronization, and collaborative editing.
- TodoView total render time: < 500ms
- Editor total render time: < 500ms
- Room switch time: < 500ms
useSyncedStore
just adds a listener on any valid proxied object. This means that property access either insideuseSyncedStore
or outside work similarly (e.g.useSyncedStore(store.todos[0])
vsuseSyncedStore(store).todos[0]
)Editor.tsx
is an exception here
- If a value is conditionally accessed, and during a render it is not accessed, it stops being updated for future renders.
- Accessing an array (e.g.
state.todos
) can result in either a shallow or deep listener depending on what properties were accessed- In
state.todos.length
, it is a shallow listener - In
state.todos.map(t => t.focus)
, it is a deep listener, but only on thetodo.focus
property
- In
Notes:
-
It appears to work like a hook, in that it remembers where a property accessor was used and triggers a rerender on that (via YJS event listeners)
-
For better performance, I may want to use YJS event listeners if I only need say, additions/deletions in a list (not nested updates).
-
To prevent the whole component rerendering for a small change in item,refactor components such that each component is only listening to the dependencies it needs.
-
Memoize child components to prevent them re-rendering when the parent re-renders.
-
Properties with frequent updates - use
useDeferredValue
/useTransition
(but only if values are memoizable - see below) -
Simple properties e.g.
boolean
,number
can be used as memoization dependencies.- Can they be deferred?
-
If you pass a proxied value to a child,
useSyncedStore
is necessary to set the event listener on the child. For example, the following will not update to changes intodo.focus
:const MyComponent = ({ todo }: { todo: Todo }) => { return todo.focus ? "focus" : "not focus"; };
Instead, do:
const MyComponent = ({ todo: _todo }: { todo: Todo }) => { const todo = useSyncedStore(_todo); return todo.focus ? "focus" : "not focus"; };
- For complex properties e.g.
todo.content
, merely accessing the value will not trigger a re-render - it needs to be manually subscribed to withobserveDeep
. - In
<StrictMode>
, this will cause a double-render (once on the component mount, again withinuseSyncedStore
as it callsforceUpdate
internally when attaching the observer). It will not affect production.
- For complex properties e.g.
Tooltip causes React.memo
to fail presumably because it uses React.cloneElement
on its children.
Benchmark (for 10,000 objects)
Array.forEach
(syncedStore proxy) - 100msgetYjsValue().toArray().forEach
(raw Yjs) - 50msObject.entries(YMap).forEach
- 200ms[...getYjsValue(YMap).entries()].forEach
- 50ms
Others:
Array.find
(within SyncedStore proxied objects) - around 20ms for 1000 objects
- When using
<Profiler>
in a mapped array, you must also give it akey
otherwise children will re-render whenever their parent does. - The Profiler does not include time spent in callbacks (e.g. in ListView)