npx claudepluginhub litun/decomposeclaudeplugin --plugin decomposeThis skill uses the workspace's default tool permissions.
You are helping write a Decompose component. Follow these patterns exactly.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Share bugs, ideas, or general feedback.
You are helping write a Decompose component. Follow these patterns exactly.
Always use interface + DefaultXxxComponent. Never extend a library base class.
interface CounterComponent {
val model: Value<Model>
fun onIncrementClicked()
data class Model(val count: Int = 0)
}
class DefaultCounterComponent(
componentContext: ComponentContext,
private val onFinished: () -> Unit, // callbacks to parent go via constructor
) : CounterComponent, ComponentContext by componentContext { // <-- delegation, not inheritance
private val _model = MutableValue(CounterComponent.Model())
override val model: Value<CounterComponent.Model> = _model
override fun onIncrementClicked() {
_model.update { it.copy(count = it.count + 1) }
}
}
Rules:
ComponentContext by componentContext — always delegate, never extendcomponentContext: ComponentContext as first parameterValue<T> (immutable), hold MutableValue<T> privatelyMutableValue.update { } for state mutations — call only on the main threadValue<T> is Decompose's multiplatform observable. Prefer it for cross-platform components.
// Good — works on all platforms, observable in Compose/SwiftUI/React
val state: Value<State> = _state
// Also acceptable if you're coroutines-only (Android/JVM):
val state: StateFlow<State>
Value is NOT a coroutine — no collect, use subscribe/subscribeAsState() in Compose.
Components get lifecycle automatically. Subscribe only in init or lifecycle callbacks.
class DefaultSomeComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
init {
lifecycle.doOnStart { /* start polling */ }
lifecycle.doOnStop { /* stop polling */ }
lifecycle.doOnDestroy { /* cleanup */ }
}
}
Lifecycle states: INITIALIZED → CREATED → STARTED → RESUMED → STOPPED → DESTROYED
RESUMEDCREATED (still alive, stopped)@Serializable
private data class State(val query: String = "", val selectedId: Long? = null)
class DefaultSearchComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private var state: State by saveable(serializer = State.serializer(), init = ::State)
}
Manual version:
private var state = stateKeeper.consume("STATE", State.serializer()) ?: State()
init {
stateKeeper.register("STATE", State.serializer()) { state }
}
Rules: @Serializable required, keep state small (<500KB on Android), consume() only once.
class DefaultTimerComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val timer = retainedInstance { Timer() }
private class Timer : InstanceKeeper.Instance {
override fun onDestroy() {}
}
}
Rules: NOT inner class, no Activity/Context/View references, implement onDestroy().
class DefaultComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val logic by saveable(serializer = Logic.State.serializer(), state = { it.state }) { savedState ->
retainedInstance { Logic(savedState) }
}
private class Logic(savedState: Logic.State?) : InstanceKeeper.Instance {
var state = savedState ?: Logic.State()
private set
@Serializable data class State(val items: List<String> = emptyList())
override fun onDestroy() {}
}
}
class DefaultEditorComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val backCallback = BackCallback(isEnabled = false) {
showDiscardDialog()
}
init { backHandler.register(backCallback) }
fun onFormChanged() { backCallback.isEnabled = true }
}
Priority: last-registered wins. Use priority = Int.MAX_VALUE to always intercept first.
class PreviewCounterComponent : CounterComponent {
override val model: Value<Model> = MutableValue(Model(count = 42))
override fun onIncrementClicked() {}
}
internal val PreviewContext: ComponentContext = DefaultComponentContext(LifecycleRegistry())
@Composable functiondefaultComponentContext() must be called only once per Activity/Fragment lifetimeretainedComponent() has the same restriction — call only once in onCreateMutableValue: subscribe and update only on the main thread — off-thread calls cause race conditions| Need | API |
|---|---|
| Component context delegation | class Foo(ctx: ComponentContext) : ComponentContext by ctx |
| Observable state | MutableValue<T> / Value<T> |
| Update state | _state.update { it.copy(...) } |
| Survive config change | retainedInstance { } |
| Survive process death | saveable(serializer = ...) or stateKeeper |
| Lifecycle events | lifecycle.doOnStart/Stop/Destroy { } |
| Custom back handling | backHandler.register(BackCallback { }) |
| Auto back in navigation | handleBackButton = true in childStack()/childSlot() |