cranpose_core/hooks.rs
1use std::{hash::Hash, rc::Rc};
2
3use crate::{
4 composer_context,
5 owned::Owned,
6 runtime,
7 state::{
8 DerivedState, MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State,
9 StructuralEqual,
10 },
11};
12
13/// Returns the value `init` made on the first composition at this call site. Later compositions
14/// return the same value until the call site leaves the composition. Compose's `remember`.
15#[track_caller]
16pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Owned<T> {
17 let source = crate::caller_location_key();
18 composer_context::with_composer(|composer| composer.remember_at(source, init))
19}
20
21/// Like [`remember`], but recomputes whenever `key` changes.
22///
23/// A plain `remember` is keyed only by composition position: when a slot is
24/// reused with different inputs (a list row that changes identity, an icon
25/// whose path prop changes), the stale value survives. `rememberKeyed`
26/// stores the key beside the value and re-runs `init` on mismatch — the JC
27/// `remember(key1) { ... }` contract.
28#[expect(non_snake_case, reason = "names follow Compose's casing")]
29#[track_caller]
30pub fn rememberKeyed<K, T>(key: K, init: impl FnOnce(&K) -> T) -> T
31where
32 K: PartialEq + 'static,
33 T: Clone + 'static,
34{
35 let slot = remember(|| std::cell::RefCell::new(None::<(K, T)>));
36 slot.with(|cell| {
37 let mut stored = cell.borrow_mut();
38 match &*stored {
39 Some((stored_key, value)) if *stored_key == key => value.clone(),
40 _ => {
41 let value = init(&key);
42 *stored = Some((key, value.clone()));
43 value
44 }
45 }
46 })
47}
48
49/// Returns a [`MutableState`] that always holds the latest value.
50///
51/// The state **reference** is stable across recompositions; only the **value** updates.
52/// This allows closures to capture a stable reference while reading fresh values.
53///
54/// # Use Case
55/// Use when a `remember`ed closure needs to read a value that changes each recomposition
56/// without recreating the closure itself.
57///
58/// # Example
59/// ```rust,ignore
60/// let config = build_config(); // Rebuilt each recomposition
61/// let config_state = rememberUpdatedState(config);
62///
63/// // This closure is created once, reads latest config via state
64/// let callback = remember(|| {
65/// let cfg = config_state;
66/// Rc::new(move || do_something(&cfg.value()))
67/// }).with(|c| c.clone());
68/// ```
69///
70/// # JC Equivalent
71/// ```kotlin
72/// @Composable
73/// fun <T> rememberUpdatedState(newValue: T): State<T> =
74/// remember { mutableStateOf(newValue) }.apply { value = newValue }
75/// ```
76#[expect(non_snake_case, reason = "names follow Compose's casing")]
77#[track_caller]
78pub fn rememberUpdatedState<T: Clone + 'static>(value: T) -> MutableState<T> {
79 let source = crate::caller_location_key();
80 composer_context::with_composer(|composer| {
81 let runtime = composer.runtime_handle();
82 let state = composer.remember_at(source, || {
83 OwnedMutableState::with_runtime(value.clone(), runtime)
84 });
85 state.with(|s| {
86 s.set(value);
87 s.handle()
88 })
89 })
90}
91
92/// Creates a new `MutableState` initialized with the given value.
93///
94/// `MutableState` is a cheap copyable observable handle. Reads are tracked by the
95/// current composer or snapshot, and writes trigger recomposition of scopes that
96/// read it.
97///
98/// Writing a value equal to the one already there is not a change, so it does
99/// not recompose anything -- Jetpack Compose's `structuralEqualityPolicy()`,
100/// which is its default too. Use [`mutableStateOfNeverEqual`] for a value that
101/// cannot be compared, or one whose every write must count.
102///
103/// # When to use
104/// Use `mutableStateOf` when:
105/// 1. You are creating state properties inside a struct or class (not a composable function).
106/// 2. You are implementing a custom state management solution.
107///
108/// **Inside a `#[composable]` function this must sit inside a `remember`**,
109/// either [`rememberMutableStateOf`] for a single state or [`remember`] around
110/// the struct that holds several. Called straight from a composable body it
111/// makes a new state every pass and loses the value, exactly as in Kotlin.
112///
113/// # Example
114///
115/// ```rust,ignore
116/// struct MyViewModel {
117/// name: MutableState<String>,
118/// }
119///
120/// impl MyViewModel {
121/// fn new() -> Self {
122/// Self {
123/// name: mutableStateOf(String::from("Alice")),
124/// }
125/// }
126/// }
127///
128/// #[composable]
129/// fn rememberMyViewModel() -> MyViewModel {
130/// remember(MyViewModel::new).with(|model| model.clone())
131/// }
132/// ```
133///
134/// A state made while a [`remember`] is building its value belongs to that
135/// slot and is released with it, which is what lets the struct above be
136/// remembered whole. Made anywhere else it is owned by the runtime and lives
137/// as long as the runtime does; to tie that to a Rust owner instead, store an
138/// [`OwnedMutableState`] or call [`MutableState::retain`] on the handle.
139#[expect(non_snake_case, reason = "names follow Compose's casing")]
140pub fn mutableStateOf<T: Clone + PartialEq + 'static>(initial: T) -> MutableState<T> {
141 current_runtime("mutableStateOf")
142 .alloc_persistent_state_with_policy(initial, Rc::new(StructuralEqual))
143}
144
145/// Like [`mutableStateOf`], for a value that cannot be compared or whose every
146/// write must count as a change.
147///
148/// This is Jetpack Compose's `neverEqualPolicy()`, and the non-remembered form
149/// of [`rememberMutableStateOfNeverEqual`].
150#[expect(non_snake_case, reason = "names follow Compose's casing")]
151pub fn mutableStateOfNeverEqual<T: Clone + 'static>(initial: T) -> MutableState<T> {
152 current_runtime("mutableStateOfNeverEqual").alloc_persistent_state(initial)
153}
154
155/// Creates an [`OwnedMutableState`] holding `initial`, which is released when its owner is dropped.
156/// Writing an equal value is not a change. Panics without an active runtime.
157#[expect(non_snake_case, reason = "names follow Compose's casing")]
158pub fn ownedMutableStateOf<T: Clone + PartialEq + 'static>(initial: T) -> OwnedMutableState<T> {
159 OwnedMutableState::with_runtime_structural_eq(initial, current_runtime("ownedMutableStateOf"))
160}
161
162/// Like [`ownedMutableStateOf`], for a value that cannot be compared.
163#[expect(non_snake_case, reason = "names follow Compose's casing")]
164pub fn ownedMutableStateOfNeverEqual<T: Clone + 'static>(initial: T) -> OwnedMutableState<T> {
165 OwnedMutableState::with_runtime(initial, current_runtime("ownedMutableStateOfNeverEqual"))
166}
167
168pub(crate) fn current_runtime(what: &str) -> runtime::RuntimeHandle {
169 composer_context::try_with_composer(super::composer::Composer::runtime_handle)
170 .or_else(runtime::current_runtime_handle)
171 .unwrap_or_else(|| {
172 panic!(
173 "{what} requires an active runtime. Create state inside a composition or after a Runtime is created."
174 )
175 })
176}
177
178/// Like [`mutableStateOf`] but returns `None` if no runtime is available.
179///
180/// Use this when you want to lazily initialize reactive state and gracefully
181/// handle the case where the runtime isn't yet available.
182#[expect(non_snake_case, reason = "names follow Compose's casing")]
183pub fn try_mutableStateOf<T: Clone + PartialEq + 'static>(initial: T) -> Option<MutableState<T>> {
184 let runtime = composer_context::try_with_composer(super::composer::Composer::runtime_handle)
185 .or_else(runtime::current_runtime_handle)?;
186 Some(runtime.alloc_persistent_state_with_policy(initial, Rc::new(StructuralEqual)))
187}
188
189/// Creates a snapshot-aware list holding `values`. Must be called during composition. Compose's
190/// `mutableStateListOf`.
191#[expect(non_snake_case, reason = "names follow Compose's casing")]
192pub fn mutableStateListOf<T, I>(values: I) -> SnapshotStateList<T>
193where
194 T: Clone + 'static,
195 I: IntoIterator<Item = T>,
196{
197 composer_context::with_composer(move |composer| composer.mutable_state_list_of(values))
198}
199
200/// Creates an empty snapshot-aware list. Must be called during composition.
201#[expect(non_snake_case, reason = "names follow Compose's casing")]
202pub fn mutableStateList<T: Clone + 'static>() -> SnapshotStateList<T> {
203 mutableStateListOf(std::iter::empty::<T>())
204}
205
206/// Creates a snapshot-aware map holding `pairs`. Must be called during composition. Compose's
207/// `mutableStateMapOf`.
208#[expect(non_snake_case, reason = "names follow Compose's casing")]
209pub fn mutableStateMapOf<K, V, I>(pairs: I) -> SnapshotStateMap<K, V>
210where
211 K: Clone + Eq + Hash + 'static,
212 V: Clone + 'static,
213 I: IntoIterator<Item = (K, V)>,
214{
215 composer_context::with_composer(move |composer| composer.mutable_state_map_of(pairs))
216}
217
218/// Creates an empty snapshot-aware map. Must be called during composition.
219#[expect(non_snake_case, reason = "names follow Compose's casing")]
220pub fn mutableStateMap<K, V>() -> SnapshotStateMap<K, V>
221where
222 K: Clone + Eq + Hash + 'static,
223 V: Clone + 'static,
224{
225 mutableStateMapOf(std::iter::empty::<(K, V)>())
226}
227
228/// A composable hook that creates and remembers a `MutableState`.
229///
230/// This is the primary way to define local state in a composable function.
231/// It combines `remember` and `mutableStateOf`.
232///
233/// # Arguments
234///
235/// * `init` - A closure that provides the initial value. This is only called once
236/// when the composable enters the composition.
237///
238/// # Example
239///
240/// ```rust,ignore
241/// #[composable]
242/// fn Counter() {
243/// // "count" persists across recompositions.
244/// // If we used mutableStateOf directly, it would reset to 0 every frame.
245/// let count = rememberMutableStateOf(|| 0);
246///
247/// Button(
248/// Modifier::empty(),
249/// ButtonSpec::default(),
250/// move || count.set(count.value() + 1),
251/// || Text(format!("Count: {}", count.value()))
252/// );
253/// }
254/// ```
255#[expect(non_snake_case, reason = "names follow Compose's casing")]
256#[track_caller]
257pub fn rememberMutableStateOf<T: Clone + PartialEq + 'static>(
258 init: impl FnOnce() -> T,
259) -> MutableState<T> {
260 let source = crate::caller_location_key();
261 composer_context::with_composer(|composer| {
262 let runtime = composer.runtime_handle();
263 composer
264 .remember_at(source, || {
265 OwnedMutableState::with_runtime_structural_eq(init(), runtime)
266 })
267 .with(OwnedMutableState::handle)
268 })
269}
270
271/// Like [`rememberMutableStateOf`], for a value that cannot be compared or whose every write must
272/// count as a change.
273#[expect(non_snake_case, reason = "names follow Compose's casing")]
274#[track_caller]
275pub fn rememberMutableStateOfNeverEqual<T: Clone + 'static>(
276 init: impl FnOnce() -> T,
277) -> MutableState<T> {
278 let source = crate::caller_location_key();
279 composer_context::with_composer(|composer| {
280 let runtime = composer.runtime_handle();
281 composer
282 .remember_at(source, || OwnedMutableState::with_runtime(init(), runtime))
283 .with(OwnedMutableState::handle)
284 })
285}
286
287/// A state computed from other states, as in Kotlin.
288///
289/// `compute` runs in a scope of its own: the states it reads subscribe that
290/// scope, not the composable reading the result, and a change to them runs
291/// `compute` again without recomposing anyone. The readers recompose only
292/// when the result differs from the last one. A composable that reads
293/// `derivedStateOf(|| index_under(offset.get()))` as an offset animates
294/// recomposes once per new index rather than once per frame.
295///
296/// A read between a write to a state `compute` read and the next
297/// recomposition, as in an event handler or an effect, runs `compute` first
298/// and sees the new result.
299#[expect(non_snake_case, reason = "names follow Compose's casing")]
300#[track_caller]
301pub fn derivedStateOf<T: 'static + Clone + PartialEq>(
302 compute: impl Fn() -> T + 'static,
303) -> State<T> {
304 let source = crate::caller_location_key();
305 let compute: Rc<dyn Fn() -> T> = Rc::new(compute);
306 composer_context::with_composer(|composer| {
307 composer.with_group(source, |composer| {
308 let derivation = composer.current_recompose_scope();
309 if let Some(scope) = &derivation
310 && !scope.is_derivation()
311 {
312 scope.mark_derivation();
313 // A read subscribes the innermost scope with a callback: the
314 // first derivation's reads would subscribe the reader.
315 composer.set_recompose_fn(|_| {});
316 }
317 let runtime = composer.runtime_handle();
318 let (state, rerun) = composer
319 .remember_at(source, || None::<DerivedState<T>>)
320 .update(|slot| {
321 let derived = match slot.take() {
322 Some(derived) => {
323 derived.set_compute(compute);
324 if restarted_for(composer, &derived) {
325 // The derivation keeps the states it read.
326 if let Some(scope) = &derivation {
327 scope.keep_reads();
328 }
329 } else {
330 derived.recompute();
331 }
332 derived
333 }
334 None => DerivedState::new(runtime, compute, derivation.as_ref()),
335 };
336 let state = derived.state();
337 let rerun = derived.rerun();
338 *slot = Some(derived);
339 (state, rerun)
340 });
341 composer.set_recompose_callback(rerun);
342 state
343 })
344 })
345}
346
347// Whether the scope whose body runs again from its own invalidation runs
348// only because `derived` took a new value. The bodies it runs see the
349// inputs they saw before, and the closure the owner passes derives what the
350// one before did: the value stays.
351fn restarted_for<T: Clone + PartialEq + 'static>(
352 composer: &crate::Composer,
353 derived: &DerivedState<T>,
354) -> bool {
355 composer
356 .restarted_scope()
357 .is_some_and(|scope| scope.invalidated_only_by_state(derived.state_id()))
358}