Skip to main content

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}