Skip to main content

cranpose_core/
hooks.rs

1use std::{hash::Hash, rc::Rc};
2
3use crate::{
4    composer_context, location_key,
5    owned::Owned,
6    runtime,
7    state::{
8        DerivedState, MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State,
9    },
10};
11
12pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Owned<T> {
13    composer_context::with_composer(|composer| composer.remember(init))
14}
15
16/// Like [`remember`], but recomputes whenever `key` changes.
17///
18/// A plain `remember` is keyed only by composition position: when a slot is
19/// reused with different inputs (a list row that changes identity, an icon
20/// whose path prop changes), the stale value survives. `rememberKeyed`
21/// stores the key beside the value and re-runs `init` on mismatch — the JC
22/// `remember(key1) { ... }` contract.
23#[allow(non_snake_case)]
24pub fn rememberKeyed<K, T>(key: K, init: impl FnOnce(&K) -> T) -> T
25where
26    K: PartialEq + 'static,
27    T: Clone + 'static,
28{
29    let slot = remember(|| std::cell::RefCell::new(None::<(K, T)>));
30    slot.with(|cell| {
31        let mut stored = cell.borrow_mut();
32        match &*stored {
33            Some((stored_key, value)) if *stored_key == key => value.clone(),
34            _ => {
35                let value = init(&key);
36                *stored = Some((key, value.clone()));
37                value
38            }
39        }
40    })
41}
42
43/// Returns a [`MutableState`] that always holds the latest value.
44///
45/// The state **reference** is stable across recompositions; only the **value** updates.
46/// This allows closures to capture a stable reference while reading fresh values.
47///
48/// # Use Case
49/// Use when a `remember`ed closure needs to read a value that changes each recomposition
50/// without recreating the closure itself.
51///
52/// # Example
53/// ```rust,ignore
54/// let config = build_config(); // Rebuilt each recomposition
55/// let config_state = rememberUpdatedState(config);
56///
57/// // This closure is created once, reads latest config via state
58/// let callback = remember(|| {
59///     let cfg = config_state;
60///     Rc::new(move || do_something(&cfg.value()))
61/// }).with(|c| c.clone());
62/// ```
63///
64/// # JC Equivalent
65/// ```kotlin
66/// @Composable
67/// fun <T> rememberUpdatedState(newValue: T): State<T> =
68///     remember { mutableStateOf(newValue) }.apply { value = newValue }
69/// ```
70#[allow(non_snake_case)]
71pub fn rememberUpdatedState<T: Clone + 'static>(value: T) -> MutableState<T> {
72    composer_context::with_composer(|composer| {
73        let runtime = composer.runtime_handle();
74        let state = composer.remember(|| OwnedMutableState::with_runtime(value.clone(), runtime));
75        state.with(|s| {
76            s.set(value);
77            s.handle()
78        })
79    })
80}
81
82#[cfg(feature = "internal")]
83#[allow(non_snake_case)]
84pub fn withFrameNanos(
85    callback: impl FnOnce(u64) + 'static,
86) -> crate::internal::FrameCallbackRegistration {
87    composer_context::with_composer(|composer| {
88        composer
89            .runtime_handle()
90            .frame_clock()
91            .with_frame_nanos(callback)
92    })
93}
94
95#[cfg(feature = "internal")]
96#[allow(non_snake_case)]
97pub fn withFrameMillis(
98    callback: impl FnOnce(u64) + 'static,
99) -> crate::internal::FrameCallbackRegistration {
100    composer_context::with_composer(|composer| {
101        composer
102            .runtime_handle()
103            .frame_clock()
104            .with_frame_millis(callback)
105    })
106}
107
108/// Creates a new `MutableState` initialized with the given value.
109///
110/// `MutableState` is a cheap copyable observable handle. Reads are tracked by the
111/// current composer or snapshot, and writes trigger recomposition of scopes that
112/// read it.
113///
114/// # When to use
115/// Use `mutableStateOf` when:
116/// 1.  You are creating state properties inside a struct or class (not a composable function).
117/// 2.  You are implementing a custom state management solution.
118///
119/// **If you are inside a `#[composable]` function, use [`rememberMutableStateOf`] instead.**
120/// `rememberMutableStateOf` wraps `mutableStateOf` in `remember`, ensuring the state persists
121/// across recompositions. Using `mutableStateOf` directly in a composable will
122/// recreated the state on every frame, losing data.
123///
124/// # Example
125///
126/// ```rust,ignore
127/// struct MyViewModel {
128///     name: MutableState<String>,
129/// }
130///
131/// impl MyViewModel {
132///     fn new() -> Self {
133///         Self {
134///             name: mutableStateOf("Alice".into()),
135///         }
136///     }
137/// }
138/// ```
139///
140/// This creates a runtime-owned persistent state. If you need the state lifetime
141/// tied to a Rust owner instead, store an [`OwnedMutableState`] or call
142/// [`MutableState::retain`] on a handle returned by [`rememberMutableStateOf`].
143#[allow(non_snake_case)]
144pub fn mutableStateOf<T: Clone + 'static>(initial: T) -> MutableState<T> {
145    let runtime = composer_context::try_with_composer(|composer| composer.runtime_handle())
146        .or_else(runtime::current_runtime_handle)
147        .expect("mutableStateOf requires an active runtime. Create state inside a composition or after a Runtime is created.");
148    runtime.alloc_persistent_state(initial)
149}
150
151#[allow(non_snake_case)]
152pub fn ownedMutableStateOf<T: Clone + 'static>(initial: T) -> OwnedMutableState<T> {
153    let runtime = composer_context::try_with_composer(|composer| composer.runtime_handle())
154        .or_else(runtime::current_runtime_handle)
155        .expect("ownedMutableStateOf requires an active runtime. Create state inside a composition or after a Runtime is created.");
156    OwnedMutableState::with_runtime(initial, runtime)
157}
158
159/// Like [`mutableStateOf`] but returns `None` if no runtime is available.
160///
161/// Use this when you want to lazily initialize reactive state and gracefully
162/// handle the case where the runtime isn't yet available.
163#[allow(non_snake_case)]
164pub fn try_mutableStateOf<T: Clone + 'static>(initial: T) -> Option<MutableState<T>> {
165    let runtime = composer_context::try_with_composer(|composer| composer.runtime_handle())
166        .or_else(runtime::current_runtime_handle)?;
167    Some(runtime.alloc_persistent_state(initial))
168}
169
170#[allow(non_snake_case)]
171pub fn mutableStateListOf<T, I>(values: I) -> SnapshotStateList<T>
172where
173    T: Clone + 'static,
174    I: IntoIterator<Item = T>,
175{
176    composer_context::with_composer(move |composer| composer.mutable_state_list_of(values))
177}
178
179#[allow(non_snake_case)]
180pub fn mutableStateList<T: Clone + 'static>() -> SnapshotStateList<T> {
181    mutableStateListOf(std::iter::empty::<T>())
182}
183
184#[allow(non_snake_case)]
185pub fn mutableStateMapOf<K, V, I>(pairs: I) -> SnapshotStateMap<K, V>
186where
187    K: Clone + Eq + Hash + 'static,
188    V: Clone + 'static,
189    I: IntoIterator<Item = (K, V)>,
190{
191    composer_context::with_composer(move |composer| composer.mutable_state_map_of(pairs))
192}
193
194#[allow(non_snake_case)]
195pub fn mutableStateMap<K, V>() -> SnapshotStateMap<K, V>
196where
197    K: Clone + Eq + Hash + 'static,
198    V: Clone + 'static,
199{
200    mutableStateMapOf(std::iter::empty::<(K, V)>())
201}
202
203/// A composable hook that creates and remembers a `MutableState`.
204///
205/// This is the primary way to define local state in a composable function.
206/// It combines `remember` and `mutableStateOf`.
207///
208/// # Arguments
209///
210/// * `init` - A closure that provides the initial value. This is only called once
211///   when the composable enters the composition.
212///
213/// # Example
214///
215/// ```rust,ignore
216/// #[composable]
217/// fn Counter() {
218///     // "count" persists across recompositions.
219///     // If we used mutableStateOf directly, it would reset to 0 every frame.
220///     let count = rememberMutableStateOf(|| 0);
221///
222///     Button(
223///         Modifier::empty(),
224///         ButtonSpec::default(),
225///         move || count.set(count.value() + 1),
226///         || Text(format!("Count: {}", count.value()))
227///     );
228/// }
229/// ```
230#[allow(non_snake_case)]
231pub fn rememberMutableStateOf<T: Clone + PartialEq + 'static>(
232    init: impl FnOnce() -> T,
233) -> MutableState<T> {
234    composer_context::with_composer(|composer| {
235        let runtime = composer.runtime_handle();
236        composer
237            .remember(|| OwnedMutableState::with_runtime_structural_eq(init(), runtime))
238            .with(|state| state.handle())
239    })
240}
241
242#[allow(non_snake_case)]
243pub fn rememberMutableStateOfNeverEqual<T: Clone + 'static>(
244    init: impl FnOnce() -> T,
245) -> MutableState<T> {
246    composer_context::with_composer(|composer| {
247        let runtime = composer.runtime_handle();
248        composer
249            .remember(|| OwnedMutableState::with_runtime(init(), runtime))
250            .with(|state| state.handle())
251    })
252}
253
254#[allow(non_snake_case)]
255pub fn derivedStateOf<T: 'static + Clone>(compute: impl Fn() -> T + 'static) -> State<T> {
256    composer_context::with_composer(|composer| {
257        let key = location_key(file!(), line!(), column!());
258        composer.with_group(key, |composer| {
259            let should_recompute = composer
260                .current_recompose_scope()
261                .map(|scope| scope.should_recompose())
262                .unwrap_or(true);
263            let runtime = composer.runtime_handle();
264            let compute_rc: Rc<dyn Fn() -> T> = Rc::new(compute);
265            let derived =
266                composer.remember(|| DerivedState::new(runtime.clone(), compute_rc.clone()));
267            derived.update(|derived| {
268                derived.set_compute(compute_rc.clone());
269                if should_recompute {
270                    derived.recompute();
271                }
272            });
273            derived.with(|derived| derived.state.as_state())
274        })
275    })
276}