
Compose State Deferred Reads
Stop Jetpack Compose scroll and animation jank by moving frame-rate State reads out of composition into layout and draw.
Install
npx skills add https://github.com/chrisbanes/skills --skill compose-state-deferred-readsWhat is this skill?
- Explains composition → layout → draw invalidation when State is read in the wrong phase
- Covers back-writing: observable updates from layout/draw that force extra composition passes
- Guides deferring scroll, animation, and gesture State into layout/draw callbacks and provider lambdas
- Applies when value-form modifiers (offset, size) receive changing animated or scroll values
- Structural fix pattern: capture measurements in callbacks instead of sibling composable body reads
Adoption & trust: 383 installs on skills.sh; 745 GitHub stars; 3/3 security scanners passed (skills.sh audits); trending (+100% hot-view momentum).
Recommended Skills
Vercel React Native Skillsvercel-labs/agent-skills
Firebase Basicsfirebase/agent-skills
Building Native Uiexpo/skills
Firebase Ai Logic Basicsfirebase/agent-skills
Native Data Fetchingexpo/skills
Firebase Firestorefirebase/agent-skills
Journey fit
Common Questions / FAQ
Is Compose State Deferred Reads safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.
SKILL.md
READMESKILL.md - Compose State Deferred Reads
# Compose state deferred reads ## Core principle State reads invalidate the phase that reads them. If a `State<T>` is read in a composable body, changes invalidate composition. If it is read in layout or draw, changes can invalidate only layout or draw. Frame-rate state such as scroll offsets, animations, and drag positions usually belongs in layout/draw, not composition. **Back-writing** is the symmetric failure mode: writing observable state from a phase that triggers invalidation of an earlier phase. Compose phases run composition → layout → draw. Writing snapshot-backed state from layout or draw to state read in composition invalidates composition; writing during composition to state read earlier in the same composition does the same. Both schedule extra work — often cascading into sibling lazy items. The fix is structural: keep the `State<T>` or a provider lambda and read the value inside a layout/draw callback; capture measurements in callbacks and apply them in the measure phase, not by reading measurement state in sibling composable bodies. ## When to use this skill - `val x by animate*AsState(...)` is passed to `Modifier.offset(x = ...)`, `Modifier.size(...)`, `Modifier.graphicsLayer(...)`, or another value-form modifier. - `LazyListState.firstVisibleItemScrollOffset`, `ScrollState.value`, `Animatable.value`, or gesture state is read in a composable body. - A composable takes `scrollOffset: Int`, `progress: Float`, `dragOffset: Offset`, or similar frame-rate values. - Recomposition counters climb during scroll, animation, or gestures even when data is stable. - A composable body calls `stateMap[key] = …`, `list.addAll(…)`, or similar on every recomposition (back-writing composition → composition). - One lazy item captures size with `onSizeChanged` / `onGloballyPositioned` and a sibling reads that height in composition (`Modifier.height(state.dp)`) — back-writing layout → composition. ## 0. Back-writing **Back-writing** = writing observable state in one phase that triggers invalidation of an earlier (or the current) phase. Compose runs composition → layout → draw, so: - Writing snapshot state during composition that's read in the same composition. - Writing snapshot state during layout (e.g. from `Modifier.layout`, `onSizeChanged`, `onGloballyPositioned`) that's read during composition. - Writing snapshot state during draw that's read during composition or layout. In all cases the writer schedules extra invalidation passes — often cascading into sibling lazy items. Do not write to `mutableStateOf`, `mutableStateListOf`, `mutableStateMapOf`, or other snapshot-backed state from the composable body on every pass: ```kotlin // ❌ BAD — mutates observable map during composition; siblings recompose repeatedly @Composable fun MergeOverlay(parent: Map<Key, ViewState>, overlay: Map<Key, ViewState>): Map<Key, ViewState> { val merged = remember { mutableStateMapOf<Key, ViewState>() } merged.clear() merged.putAll(parent) merged.putAll(overlay) // back-writing composition → composition return merged } // ✅ GOOD — read-only merge; no composition-time writes @Composable fun MergeOverlay(parent: Map<Key, ViewState>, overlay: Map<Key, ViewState>): Map<Key, ViewState> = remember(parent, overlay) { if (overlay.isEmpty()) parent else parent + overlay } ``` Prefer `remember(keys) { … }` for derived read-only snapshots. Reserve `mutableState*` writes for event callbacks (`onClick`) or effects — not for rebuilding derived data on every composition. Callbacks like `onSizeChanged` write *during layout*. That is only safe if no earlier phase reads the resulting sta