dioxus_provider/mutation.rs
1//! # Mutation System for Dioxus Provider
2//!
3//! This module provides mutation capabilities for modifying data and keeping caches in sync.
4//! It integrates seamlessly with the provider system for automatic cache invalidation and
5//! optimistic updates.
6//!
7//! ## Features
8//!
9//! - **Mutation Providers**: Define mutations with the `#[mutation]` macro
10//! - **Optimistic Updates**: Immediate UI updates with rollback on failure
11//! - **Automatic Cache Invalidation**: Invalidate related providers automatically
12//! - **Mutation State**: Track loading, success, and error states
13//! - **Rollback Support**: Automatic rollback of optimistic updates on failure
14
15use dioxus::prelude::*;
16use futures::channel::oneshot;
17use std::{collections::HashSet, future::Future};
18
19use crate::{
20 global::{get_global_cache, get_global_refresh_registry},
21 hooks::Provider,
22 types::ProviderParamBounds,
23};
24
25/// Represents the state of a mutation operation
26#[derive(Clone, PartialEq)]
27pub enum MutationState<T, E> {
28 /// The mutation is idle (not running)
29 Idle,
30 /// The mutation is currently loading
31 Loading,
32 /// The mutation completed successfully
33 Success(T),
34 /// The mutation failed with an error
35 Error(E),
36}
37
38impl<T, E> crate::state::AsyncState for MutationState<T, E> {
39 type Data = T;
40 type Error = E;
41
42 fn is_loading(&self) -> bool {
43 matches!(self, MutationState::Loading)
44 }
45
46 fn is_success(&self) -> bool {
47 matches!(self, MutationState::Success(_))
48 }
49
50 fn is_error(&self) -> bool {
51 matches!(self, MutationState::Error(_))
52 }
53
54 fn data(&self) -> Option<&T> {
55 match self {
56 MutationState::Success(data) => Some(data),
57 _ => None,
58 }
59 }
60
61 fn error(&self) -> Option<&E> {
62 match self {
63 MutationState::Error(error) => Some(error),
64 _ => None,
65 }
66 }
67}
68
69impl<T, E> MutationState<T, E> {
70 /// Returns true if the mutation is idle
71 pub fn is_idle(&self) -> bool {
72 matches!(self, MutationState::Idle)
73 }
74
75 /// Returns true if the mutation is currently loading
76 pub fn is_loading(&self) -> bool {
77 <Self as crate::state::AsyncState>::is_loading(self)
78 }
79
80 /// Returns true if the mutation completed successfully
81 pub fn is_success(&self) -> bool {
82 <Self as crate::state::AsyncState>::is_success(self)
83 }
84
85 /// Returns true if the mutation failed
86 pub fn is_error(&self) -> bool {
87 <Self as crate::state::AsyncState>::is_error(self)
88 }
89
90 /// Returns the success data if available
91 pub fn data(&self) -> Option<&T> {
92 <Self as crate::state::AsyncState>::data(self)
93 }
94
95 /// Returns the error if available
96 pub fn error(&self) -> Option<&E> {
97 <Self as crate::state::AsyncState>::error(self)
98 }
99}
100
101/// Provides access to cached mutation data for implementations generated by the macro.
102pub struct MutationContext<'a, Data, Error> {
103 current: Option<&'a Result<Data, Error>>,
104}
105
106impl<'a, Data, Error> MutationContext<'a, Data, Error> {
107 /// Create a new mutation context from the current cached result.
108 pub fn new(current: Option<&'a Result<Data, Error>>) -> Self {
109 Self { current }
110 }
111
112 /// Returns the current cached result, including the error if the cache failed previously.
113 pub fn current(&self) -> Option<&Result<Data, Error>> {
114 self.current
115 }
116
117 /// Returns a reference to the current successful cached data, if available.
118 pub fn current_success(&self) -> Option<&Data> {
119 match self.current? {
120 Ok(data) => Some(data),
121 Err(_) => None,
122 }
123 }
124
125 /// Clones the current successful cached data, if available.
126 pub fn cloned_success(&self) -> Option<Data>
127 where
128 Data: Clone,
129 {
130 self.current()?.as_ref().ok().cloned()
131 }
132
133 /// Applies a transformation to the cloned cached data and returns the updated value.
134 pub fn map_current<F>(&self, f: F) -> Option<Data>
135 where
136 Data: Clone,
137 F: FnOnce(&mut Data),
138 {
139 let mut cloned = self.cloned_success()?;
140 f(&mut cloned);
141 Some(cloned)
142 }
143
144 /// Applies a transformation to the cloned cached data, or returns a default value if no data is available.
145 ///
146 /// This is useful when you need to ensure a value is always returned, even if the cache is empty.
147 ///
148 /// ## Example
149 ///
150 /// ```rust,no_run
151 /// use dioxus_provider::prelude::*;
152 ///
153 /// #[mutation(invalidates = [fetch_items])]
154 /// async fn add_item(
155 /// item: String,
156 /// ctx: MutationContext<Vec<String>, String>,
157 /// ) -> Result<Vec<String>, String> {
158 /// Ok(ctx.map_or_else(
159 /// || vec![item.clone()], // default if no cached data
160 /// |items| {
161 /// items.push(item.clone());
162 /// }
163 /// ))
164 /// }
165 /// ```
166 pub fn map_or_else<F, D>(&self, default: D, f: F) -> Data
167 where
168 Data: Clone,
169 F: FnOnce(&mut Data),
170 D: FnOnce() -> Data,
171 {
172 self.map_current(f).unwrap_or_else(default)
173 }
174
175 /// Updates the cached data in place, returning the modified data or None if no data exists.
176 ///
177 /// This is similar to `map_current` but provides a more explicit name for mutations
178 /// that modify existing data.
179 ///
180 /// ## Example
181 ///
182 /// ```rust,no_run
183 /// use dioxus_provider::prelude::*;
184 ///
185 /// #[mutation(invalidates = [fetch_counter])]
186 /// async fn increment_counter(
187 /// ctx: MutationContext<i32, String>,
188 /// ) -> Result<i32, String> {
189 /// ctx.update_in_place(|count| *count += 1)
190 /// .ok_or_else(|| "No counter data available".to_string())
191 /// }
192 /// ```
193 pub fn update_in_place<F>(&self, f: F) -> Option<Data>
194 where
195 Data: Clone,
196 F: FnOnce(&mut Data),
197 {
198 self.map_current(f)
199 }
200
201 /// Returns true if there is currently cached data available.
202 pub fn has_data(&self) -> bool {
203 self.current_success().is_some()
204 }
205
206 /// Returns true if the current cache contains an error.
207 pub fn has_error(&self) -> bool {
208 matches!(self.current, Some(Err(_)))
209 }
210}
211
212/// Trait for defining mutations - operations that modify data
213///
214/// Mutations are similar to providers but are designed for data modification operations.
215/// They typically involve server requests to create, update, or delete data.
216///
217/// ## Usage
218/// Prefer using the `#[mutation]` macro to define mutations. Manual trait implementations are for advanced use only.
219///
220/// ## Example
221///
222/// ```rust,no_run
223/// use dioxus_provider::prelude::*;
224///
225/// #[mutation(invalidates = [fetch_user, fetch_users])]
226/// async fn update_user(user_id: u32, data: UserData) -> Result<User, String> {
227/// // Make API call to update user
228/// api_client.update_user(user_id, data).await
229/// }
230/// ```
231pub trait Mutation<Input = ()>: Clone + PartialEq + 'static
232where
233 Input: Clone + PartialEq + 'static,
234{
235 /// The type of data returned on successful mutation
236 type Output: Clone + PartialEq + Send + Sync + 'static;
237 /// The type of error returned on mutation failure
238 type Error: Clone + PartialEq + Send + Sync + 'static;
239
240 /// Execute the mutation with the given input
241 fn mutate(&self, input: Input) -> impl Future<Output = Result<Self::Output, Self::Error>>;
242
243 /// Execute the mutation with access to current cached data
244 /// This allows mutations to work with existing state instead of redefining data
245 /// If not implemented, falls back to the simple mutate method
246 fn mutate_with_current(
247 &self,
248 input: Input,
249 _current_data: Option<&Result<Self::Output, Self::Error>>,
250 ) -> impl Future<Output = Result<Self::Output, Self::Error>> {
251 // Default implementation falls back to simple mutate
252 self.mutate(input)
253 }
254
255 /// Get a unique identifier for this mutation type
256 fn id(&self) -> String {
257 std::any::type_name::<Self>().to_string()
258 }
259
260 /// Get list of provider cache keys that should be invalidated after successful mutation
261 /// Override this to specify which providers should be refreshed after mutation
262 fn invalidates(&self) -> Vec<String> {
263 Vec::new()
264 }
265
266 /// Returns true if this mutation has optimistic updates configured
267 /// Used by `use_mutation` to automatically detect and enable optimistic behavior
268 fn has_optimistic(&self) -> bool {
269 false
270 }
271
272 /// Provide optimistic cache updates for immediate UI feedback
273 /// Returns a list of (cache_key, optimistic_result) pairs to update the cache with
274 /// This allows the UI to update immediately with the expected result
275 /// The Result should contain the optimistic success value
276 fn optimistic_updates(
277 &self,
278 _input: &Input,
279 ) -> Vec<(String, Result<Self::Output, Self::Error>)> {
280 Vec::new()
281 }
282
283 /// Compute optimistic updates with access to current cached data
284 /// This is more efficient as it allows mutations to work with existing data
285 /// instead of duplicating data structures
286 ///
287 /// ## Return Value Usage
288 /// The mutation's return value is used to update the cache directly on success,
289 /// avoiding unnecessary provider refetches when using optimistic updates.
290 /// When optimistic updates are defined, the actual server response replaces
291 /// the optimistic value in the cache, keeping the UI in sync with the backend.
292 fn optimistic_updates_with_current(
293 &self,
294 _input: &Input,
295 _current_data: Option<&Result<Self::Output, Self::Error>>,
296 ) -> Vec<(String, Result<Self::Output, Self::Error>)> {
297 // Fallback to the simple method if not overridden
298 self.optimistic_updates(_input)
299 }
300}
301
302/// Type alias for the return type of mutation hooks
303pub type MutationHookResult<M, Input, F> = (
304 Signal<MutationState<<M as Mutation<Input>>::Output, <M as Mutation<Input>>::Error>>,
305 F,
306);
307
308/// Configuration for mutation behavior
309#[derive(Clone, Debug)]
310struct MutationConfig {
311 /// Whether to apply optimistic updates
312 optimistic: bool,
313}
314
315impl MutationConfig {
316 /// Create a default mutation configuration (no optimistic updates)
317 fn default() -> Self {
318 Self { optimistic: false }
319 }
320
321 /// Create a mutation configuration with optimistic updates enabled
322 fn optimistic() -> Self {
323 Self { optimistic: true }
324 }
325}
326
327/// Core mutation logic shared between use_mutation and use_optimistic_mutation
328fn mutation_core<M, Input>(
329 mutation: M,
330 config: MutationConfig,
331) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
332where
333 M: Mutation<Input> + Send + Sync + 'static,
334 Input: Clone + PartialEq + Send + Sync + 'static,
335{
336 let state = use_signal(|| MutationState::Idle);
337 let cache = get_global_cache();
338 let refresh_registry = get_global_refresh_registry();
339
340 let mutate_fn = {
341 let mutation = mutation.clone();
342 let cache = cache
343 .unwrap_or_else(|_| {
344 panic!("Global providers not initialized. Call dioxus_provider::init() before using mutations.")
345 })
346 .clone();
347 let refresh_registry = refresh_registry
348 .unwrap_or_else(|_| {
349 panic!("Global providers not initialized. Call dioxus_provider::init() before using mutations.")
350 })
351 .clone();
352 let is_optimistic = config.optimistic;
353
354 move |input: Input| {
355 // Prevent concurrent optimistic mutations
356 if is_optimistic && matches!(*state.read(), MutationState::Loading) {
357 crate::debug_log!(
358 "βΈοΈ [MUTATION] Skipping mutation - already in progress for: {}",
359 mutation.id()
360 );
361 return;
362 }
363
364 let mutation = mutation.clone();
365 let cache = cache.clone();
366 let refresh_registry = refresh_registry.clone();
367 let input = input.clone();
368 let mut ui_state = state;
369
370 // Set loading state
371 ui_state.set(MutationState::Loading);
372
373 // Collect optimistic updates if enabled
374 let cache_keys_to_check: Vec<String> = mutation.invalidates();
375 let mut optimistic_updates = Vec::new();
376
377 if is_optimistic {
378 // First, try to get optimistic updates from providers that have cached data
379 for cache_key in &cache_keys_to_check {
380 let current_data = cache.get::<Result<M::Output, M::Error>>(cache_key);
381 let updates =
382 mutation.optimistic_updates_with_current(&input, current_data.as_ref());
383 optimistic_updates.extend(updates);
384 }
385
386 // If we don't have any optimistic updates yet, try the fallback method
387 if optimistic_updates.is_empty() {
388 optimistic_updates = mutation.optimistic_updates(&input);
389 }
390
391 // If we still don't have optimistic updates, but we have cache keys to invalidate,
392 // we need to handle the case where some providers don't have cached data
393 if optimistic_updates.is_empty() && !cache_keys_to_check.is_empty() {
394 // For providers without cached data, we'll use SWR behavior:
395 // - Don't show loading state immediately
396 // - Let them fetch in the background while showing stale data (if any)
397 // - This prevents jitters for providers that don't have optimistic updates
398 crate::debug_log!(
399 "β‘ [OPTIMISTIC] No optimistic updates available, using SWR for {} cache keys",
400 cache_keys_to_check.len()
401 );
402 }
403
404 if !optimistic_updates.is_empty() {
405 crate::debug_log!(
406 "β‘ [OPTIMISTIC] Optimistically updating {} cache entries",
407 optimistic_updates.len()
408 );
409 for (cache_key, optimistic_result) in &optimistic_updates {
410 cache.set(cache_key.clone(), optimistic_result.clone());
411 refresh_registry.trigger_refresh(cache_key);
412 }
413 }
414 }
415
416 let optimistic_updates_for_rollback = optimistic_updates.clone();
417 let (result_tx, result_rx) = oneshot::channel::<Result<M::Output, M::Error>>();
418
419 spawn({
420 let mut state = ui_state;
421 async move {
422 if let Ok(outcome) = result_rx.await {
423 match outcome {
424 Ok(result) => state.set(MutationState::Success(result)),
425 Err(error) => state.set(MutationState::Error(error)),
426 }
427 }
428 }
429 });
430
431 dioxus_core::spawn_forever(async move {
432 #[cfg(feature = "tracing")]
433 let mutation_type = if is_optimistic {
434 "optimistic mutation"
435 } else {
436 "mutation"
437 };
438 crate::debug_log!(
439 "π [MUTATION] Starting {}: {}",
440 mutation_type,
441 mutation.id()
442 );
443
444 // Get current data for the mutation
445 let mutation_current_data = cache_keys_to_check
446 .first()
447 .and_then(|first_key| cache.get::<Result<M::Output, M::Error>>(first_key));
448
449 let mutation_result = mutation
450 .mutate_with_current(input, mutation_current_data.as_ref())
451 .await;
452
453 crate::debug_log!(
454 "π‘ [MUTATION] Mutation completed for: {}, result: {}",
455 mutation.id(),
456 match &mutation_result {
457 Ok(_) => "Success",
458 Err(_) => "Error",
459 }
460 );
461
462 match &mutation_result {
463 Ok(result) => {
464 crate::debug_log!("β
[MUTATION] Mutation succeeded: {}", mutation.id());
465
466 if is_optimistic && !optimistic_updates_for_rollback.is_empty() {
467 // Update optimistic caches with real result
468 let optimistic_keys: HashSet<String> = optimistic_updates_for_rollback
469 .iter()
470 .map(|(key, _)| key.clone())
471 .collect();
472
473 crate::debug_log!(
474 "π¦ [MUTATION] Updating {} optimistic cache entries with mutation result",
475 optimistic_keys.len()
476 );
477
478 for cache_key in &optimistic_keys {
479 cache.set(cache_key.clone(), Ok::<_, M::Error>(result.clone()));
480 refresh_registry.trigger_refresh(cache_key);
481 }
482
483 let invalidation_keys: Vec<_> = cache_keys_to_check
484 .iter()
485 .filter(|key| !optimistic_keys.contains(*key))
486 .cloned()
487 .collect();
488
489 if !invalidation_keys.is_empty() {
490 crate::debug_log!(
491 "π [MUTATION] Invalidating {} cache keys: {:?}",
492 invalidation_keys.len(),
493 invalidation_keys
494 );
495
496 for cache_key in invalidation_keys {
497 cache.invalidate(&cache_key);
498 refresh_registry.trigger_refresh(&cache_key);
499 }
500 }
501 } else {
502 // Standard cache invalidation
503 crate::debug_log!(
504 "π [MUTATION] Invalidating {} cache keys: {:?}",
505 cache_keys_to_check.len(),
506 cache_keys_to_check
507 );
508
509 for cache_key in &cache_keys_to_check {
510 crate::debug_log!(
511 "ποΈ [MUTATION] Invalidating cache key: {}",
512 cache_key
513 );
514 cache.invalidate(cache_key);
515 refresh_registry.trigger_refresh(cache_key);
516 }
517 }
518 }
519 Err(_) => {
520 crate::debug_log!("β [MUTATION] Mutation failed: {}", mutation.id());
521
522 if is_optimistic && !optimistic_updates_for_rollback.is_empty() {
523 crate::debug_log!(
524 "π [ROLLBACK] Rolling back {} optimistic updates",
525 optimistic_updates_for_rollback.len()
526 );
527
528 for (cache_key, _) in &optimistic_updates_for_rollback {
529 crate::debug_log!(
530 "π [ROLLBACK] Rolling back optimistic update for cache key: {}",
531 cache_key
532 );
533 cache.invalidate(cache_key);
534 refresh_registry.trigger_refresh(cache_key);
535 }
536 }
537 }
538 }
539
540 if result_tx.send(mutation_result).is_err() {
541 crate::debug_log!(
542 "β οΈ [MUTATION] Result receiver dropped before completion for: {}",
543 mutation.id()
544 );
545 }
546 });
547 }
548 };
549
550 (state, mutate_fn)
551}
552
553/// Hook to create a mutation that can be triggered manually
554///
555/// This hook automatically detects whether optimistic updates are configured
556/// in the `#[mutation]` macro and enables them accordingly. You no longer need
557/// to choose between `use_mutation` and `use_optimistic_mutation` - this hook
558/// handles both cases automatically.
559///
560/// Returns a tuple containing:
561/// 1. A signal with the current mutation state
562/// 2. A function to trigger the mutation
563///
564/// ## Example
565///
566/// ```rust,no_run
567/// use dioxus::prelude::*;
568/// use dioxus_provider::prelude::*;
569///
570/// #[component]
571/// fn UpdateUserForm(user_id: u32) -> Element {
572/// let (mutation_state, mutate) = use_mutation(update_user());
573///
574/// let handle_submit = move |data: UserData| {
575/// mutate(user_id, data);
576/// };
577///
578/// rsx! {
579/// form {
580/// button {
581/// disabled: mutation_state.read().is_loading(),
582/// onclick: move |_| handle_submit(get_form_data()),
583/// "Update User"
584/// }
585/// match &*mutation_state.read() {
586/// MutationState::Loading => rsx! { div { "Updating..." } },
587/// MutationState::Success(_) => rsx! { div { "Updated successfully!" } },
588/// MutationState::Error(err) => rsx! { div { "Error: {err}" } },
589/// MutationState::Idle => rsx! { div {} },
590/// }
591/// }
592/// }
593/// }
594/// ```
595pub fn use_mutation<M, Input>(mutation: M) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
596where
597 M: Mutation<Input> + Send + Sync + 'static,
598 Input: Clone + PartialEq + Send + Sync + 'static,
599{
600 let config = if mutation.has_optimistic() {
601 MutationConfig::optimistic()
602 } else {
603 MutationConfig::default()
604 };
605 mutation_core(mutation, config)
606}
607
608/// Hook to create a mutation with optimistic updates
609///
610/// **Note:** This hook is now an alias for `use_mutation`, which automatically detects
611/// whether optimistic updates are configured. You can use `use_mutation` directly instead.
612/// This function is maintained for backward compatibility.
613///
614/// The optimistic behavior is automatically detected from the `#[mutation]` macro's
615/// `optimistic` parameter. If an optimistic closure is provided in the macro,
616/// the mutation will automatically use optimistic updates.
617///
618/// ## Example
619///
620/// ```rust,no_run
621/// use dioxus::prelude::*;
622/// use dioxus_provider::prelude::*;
623///
624/// #[component]
625/// fn TodoItem(todo_id: u32) -> Element {
626/// // Both of these are equivalent:
627/// let (mutation_state, mutate) = use_mutation(toggle_todo());
628/// // let (mutation_state, mutate) = use_optimistic_mutation(toggle_todo());
629///
630/// rsx! {
631/// div {
632/// button {
633/// onclick: move |_| mutate(todo_id),
634/// "Toggle Todo"
635/// }
636/// match &*mutation_state.read() {
637/// MutationState::Loading => rsx! { span { "Saving..." } },
638/// MutationState::Error(err) => rsx! { span { "Error: {err}" } },
639/// _ => rsx! { span {} },
640/// }
641/// }
642/// }
643/// }
644/// ```
645pub fn use_optimistic_mutation<M, Input>(
646 mutation: M,
647) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
648where
649 M: Mutation<Input> + Send + Sync + 'static,
650 Input: Clone + PartialEq + Send + Sync + 'static,
651{
652 // Simply delegate to use_mutation, which auto-detects optimistic updates
653 use_mutation(mutation)
654}
655
656/// Helper function to create cache keys for providers with parameters
657pub fn provider_cache_key<P, Param>(provider: P, param: Param) -> String
658where
659 P: Provider<Param>,
660 Param: ProviderParamBounds,
661{
662 provider.id(¶m)
663}
664
665/// Helper function to create cache keys for providers without parameters
666pub fn provider_cache_key_simple<P>(provider: P) -> String
667where
668 P: Provider<()>,
669{
670 provider.id(&())
671}