Skip to main content

cranpose_core/
hooks.rs

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