choosing-derivedstateof — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited choosing-derivedstateof (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
derivedStateOf produces a State whose readers only invalidate when the derived result changes, even if the input states change far more often. Use it when input frequency exceeds output frequency. Use it for any other shape and it is pure overhead — an extra snapshot subscription with no filtering benefit. This skill teaches Claude when to reach for it, when to refuse, and how to avoid the canonical capture-by-initial-value pitfall.
derivedStateOf.firstVisibleItemIndex == 0, scrollState.value > threshold, "show FAB on scroll").derivedStateOf exists in the code but recomposition counts didn't drop, or the value never updates after the first composition (the capture-by-initial-value bug).Modifier.alpha(state.value)). Fix that with ../deferring-state-reads/SKILL.md first — derivedStateOf does not address phase issues.../../stability/diagnosing-compose-stability/SKILL.md."$first $last" from two name states). derivedStateOf is pure overhead in that case — use a direct read or a plain remember.@TraceRecomposition from skydoves/compose-stability-analyzer.listState.firstVisibleItemIndex (changes every list item scrolled past) → Boolean (changes once when the user crosses index 0).derivedStateOf is the right tool.derivedStateOf adds an extra snapshot subscription for nothing. Use a direct read or a plain remember(input1, input2).derivedStateOf { } would be re-created on every composition, defeating the cache. The remember is mandatory.remember(threshold) { derivedStateOf { … > threshold } }.derivedStateOf is missing, (b) the remember is missing, or (c) something else (a sibling state read, a wrong-phase modifier) is invalidating the same scope.// WRONG
val showFab = listState.firstVisibleItemIndex > 0
// WRONG because: this reads firstVisibleItemIndex on every recomposition; the consuming composable invalidates per scrolled item, not just when the boolean flips.// RIGHT
val showFab by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}firstVisibleItemIndex updates on every list item scrolled past; showFab only flips when the user crosses index 0. The remember keeps the derived state alive across compositions; the derivedStateOf filters out every input change that does not flip the boolean.
// WRONG
val first by remember { mutableStateOf("Ada") }
val last by remember { mutableStateOf("Lovelace") }
val fullName by remember { derivedStateOf { "$first $last" } }
// WRONG because: first and last change at the same rate as fullName; derivedStateOf adds a snapshot subscription with zero filtering benefit.// RIGHT — direct read, no derivedStateOf needed
val first by remember { mutableStateOf("Ada") }
val last by remember { mutableStateOf("Lovelace") }
val fullName = "$first $last"If a memoization cost concern exists (the concatenation is expensive), use remember(first, last) { computeFullName(first, last) } — derivedStateOf is still the wrong shape because there is nothing to filter.
// WRONG
@Composable
fun Header(threshold: Int, listState: LazyListState) {
val isLarge by remember {
derivedStateOf { listState.firstVisibleItemIndex > threshold }
}
// WRONG because: threshold is captured inside remember { ... } at first composition; later threshold changes are ignored, and the derivation reuses the stale value forever.
}// RIGHT — threshold is a remember key, so a new derivedStateOf is created when it changes
@Composable
fun Header(threshold: Int, listState: LazyListState) {
val isLarge by remember(threshold) {
derivedStateOf { listState.firstVisibleItemIndex > threshold }
}
}The rule: any non-State value captured by the derivedStateOf lambda MUST be a key on the surrounding remember. Otherwise the derivation locks in the value from first composition.
snapshotFlow// WRONG
@Composable
fun Feed(listState: LazyListState, onScrolledPastFold: () -> Unit) {
val pastFold by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
if (pastFold) onScrolledPastFold()
// WRONG because: side effects in a composable body run on every (re)composition, can fire multiple times for the same flip, and tie the side effect to the composition lifecycle.
}// RIGHT — fire-and-forget side effect via snapshotFlow
@Composable
fun Feed(listState: LazyListState, onScrolledPastFold: () -> Unit) {
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex > 5 }
.distinctUntilChanged()
.filter { it }
.collect { onScrolledPastFold() }
}
}snapshotFlow reads snapshot state inside a coroutine and emits when the read-set's combined value changes. The LaunchedEffect keys the collection to listState, and distinctUntilChanged() ensures one emission per flip. No restart scope is involved — the consuming composable never recomposes for this signal.
// RIGHT
val priceBucket by remember {
derivedStateOf {
when {
cartTotal.value < 10_000 -> Bucket.Small
cartTotal.value < 50_000 -> Bucket.Medium
else -> Bucket.Large
}
}
}cartTotal may tick by single won/cents; priceBucket flips at most twice across the entire range. This is the canonical use case.
derivedStateOf in remember { … }. A bare derivedStateOf { } is re-created every composition and provides no filtering.remember keys. Otherwise the derivation freezes the values from first composition.derivedStateOf when input and output frequency match. It is pure overhead; use a direct read or remember(keys) { … }.collectAsState() / collectAsStateWithLifecycle() in derivedStateOf "just to be safe" — that adds a subscription layer for nothing. Filter upstream with .distinctUntilChanged() or .map { } on the flow.snapshotFlow { … } over derivedStateOf for fire-and-forget side effects. It keeps the composition free of the signal entirely.derivedStateOf is to reduce them; if they didn't drop, something else is wrong (often a wrong-phase read — see ../deferring-state-reads/SKILL.md).derivedStateOf in the file is inside a remember { … } (or remember(keys) { … }) call.derivedStateOf lambda's captured non-state variables appear as remember keys, OR the lambda only reads State objects.LaunchedEffect { snapshotFlow { … }.collect { } }, not by reading the derived state in the composable body.derivedStateOf { shows no occurrences where input and output frequencies match (no "$first $last"-shaped uses).~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.