Choosing Derivedstateof logo

Choosing Derivedstateof

Community
skydoves
choosing-derivedstateof

Use this skill to decide when Jetpack Compose derivedStateOf is the right tool and when it is pure overhead. Covers the "input frequency must exceed output frequency" rule, the mandatory remember { derivedStateOf { } } wrapper, the canonical pitfall of capturing non-state variables by initial value (and the remember(key) fix), and the snapshotFlow alternative for fire-and-forget side effects on derived values. Use when the developer mentions derivedStateOf, scroll-position-driven booleans, threshold checks, firstVisibleItemIndex, "show FAB on scroll", recomposition counts that don't drop after wrapping a value, or asks whether a computed string concatenation should use derivedStateOf.

Overview

Publisherskydoves
Repositorycompose-performance-skills
Skill namechoosing-derivedstateof
Stars
515
Forks
19
Bundled files
Instructions only
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by skydoves on GitHub. Read the source before you install it.

Installation

Install the Choosing Derivedstateof AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/skydoves/compose-performance-skills/tree/main/recomposition/choosing-derivedstateof
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/skydoves/compose-performance-skills.git /tmp/compose-performance-skills
mkdir -p .claude/skills
cp -r /tmp/compose-performance-skills/recomposition/choosing-derivedstateof .claude/skills/choosing-derivedstateof
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Choosing Derivedstateof in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Choosing Derivedstateof on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Choosing Derivedstateof is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Choosing derivedStateOf — Filter Hot Inputs into Cold Outputs

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.

When to use this skill

  • The developer asks whether a value should be wrapped in derivedStateOf.
  • A scroll position drives a boolean (firstVisibleItemIndex == 0, scrollState.value > threshold, "show FAB on scroll").
  • A high-frequency input (drag delta, animation progress) feeds a low-frequency output (boolean threshold, bucketed integer).
  • A 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).
  • The developer wants a one-shot side effect (analytics, logging, snackbar) when a derived value flips.

When NOT to use this skill

  • The state read is in the wrong phase (e.g. Modifier.alpha(state.value)). Fix that with ../deferring-state-reads/SKILL.md first — derivedStateOf does not address phase issues.
  • Diagnosing whether a parameter is unstable. That is ../../stability/diagnosing-compose-stability/SKILL.md.
  • Input frequency equals output frequency (e.g. "$first $last" from two name states). derivedStateOf is pure overhead in that case — use a direct read or a plain remember.

Prerequisites

  • Familiarity with state-read-driven recomposition: a snapshot read inside a restartable composable subscribes that composable's restart scope to the state.
  • Compose runtime ≥ 1.0 (the API is foundational; behavior described here is stable).
  • Ability to confirm recomposition counts via Layout Inspector or @TraceRecomposition from skydoves/compose-stability-analyzer.

Workflow

  • 1. Identify the input state(s) and the value the UI actually consumes. Write down both update frequencies. Example: listState.firstVisibleItemIndex (changes every list item scrolled past) → Boolean (changes once when the user crosses index 0).

  • 2. Compare frequencies. Apply the rule:

    • Input updates much more often than output → derivedStateOf is the right tool.
    • Input and output update at roughly the same rate → derivedStateOf adds an extra snapshot subscription for nothing. Use a direct read or a plain remember(input1, input2).
  • 3. Wrap the derivation in remember { derivedStateOf { … } }. A bare derivedStateOf { } would be re-created on every composition, defeating the cache. The remember is mandatory.

  • 4. If the lambda captures non-state variables (function parameters, locals, props), pass them as remember keys. A captured non-state variable is read once at first composition and frozen forever — the derivation will silently use the stale value. The fix is remember(threshold) { derivedStateOf { … > threshold } }.

  • 5. For one-shot side effects on a derived value (logging, analytics, snackbar), prefer snapshotFlow { … }.collect { … } inside LaunchedEffect. That avoids subscribing the composable's restart scope to the derived state; the side effect runs in a coroutine, off the composition path.

  • 6. Verify in Layout Inspector / @TraceRecomposition. The consuming composable's recomposition count MUST only increment when the derived result changes — not on every input tick. If it still climbs per input tick, either (a) the 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.

Patterns

Pattern: scroll-to-top FAB — input >> output

kotlin
// 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.
kotlin
// 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.

Pattern: pure overhead — input ≈ output

kotlin
// 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.
kotlin
// 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.

Pattern: captured non-state variable — the silent bug

kotlin
// 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.
}
kotlin
// 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.

Pattern: one-shot side effect → snapshotFlow

kotlin
// 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.
}
kotlin
// 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.

Pattern: derive across many inputs to one bucket

kotlin
// 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.

Mandatory rules

  • MUST wrap every derivedStateOf in remember { … }. A bare derivedStateOf { } is re-created every composition and provides no filtering.
  • MUST add captured non-state variables (function parameters, local vals, props) as remember keys. Otherwise the derivation freezes the values from first composition.
  • MUST NOT use derivedStateOf when input and output frequency match. It is pure overhead; use a direct read or remember(keys) { … }.
  • MUST NOT wrap the result of collectAsState() / collectAsStateWithLifecycle() in derivedStateOf "just to be safe" — that adds a subscription layer for nothing. Filter upstream with .distinctUntilChanged() or .map { } on the flow.
  • PREFERRED: snapshotFlow { … } over derivedStateOf for fire-and-forget side effects. It keeps the composition free of the signal entirely.
  • PREFERRED: measure recomposition counts before AND after the change. The whole point of 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).

Verification

  • Layout Inspector shows the consuming composable recomposes only when the derived result changes (e.g. once when the FAB appears, once when it disappears) — not on every input tick.
  • Every derivedStateOf in the file is inside a remember { … } (or remember(keys) { … }) call.
  • Every derivedStateOf lambda's captured non-state variables appear as remember keys, OR the lambda only reads State objects.
  • Side effects on derived values run via LaunchedEffect { snapshotFlow { … }.collect { } }, not by reading the derived state in the composable body.
  • A grep for derivedStateOf { shows no occurrences where input and output frequencies match (no "$first $last"-shaped uses).

References

Frequently asked questions

What does the Choosing Derivedstateof AI skill do?

Use this skill to decide when Jetpack Compose derivedStateOf is the right tool and when it is pure overhead. Covers the "input frequency must exceed output frequency" rule, the mandatory remember { derivedStateOf { } } wrapper, the canonical pitfall of capturing non-state variables by initial value (and the remember(key) fix), and the snapshotFlow alternative for fire-and-forget side effects on derived values. Use when the developer mentions derivedStateOf, scroll-position-driven booleans, threshold checks, firstVisibleItemIndex, "show FAB on scroll", recomposition counts that don't drop af...

Why use Choosing Derivedstateof on TypingMind?

Because you install it once and use it with any model. Choosing Derivedstateof is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Choosing Derivedstateof in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/skydoves/compose-performance-skills/tree/main/recomposition/choosing-derivedstateof. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Choosing Derivedstateof?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Choosing Derivedstateof?

As many as you like. As long as a model supports skills, you can use Choosing Derivedstateof with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Choosing Derivedstateof AI skill free?

Yes. It is published on GitHub by skydoves under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇