dioxus_provider/hooks/
provider.rs

1//! # Provider Hooks
2//!
3//! This module provides hooks for working with providers in Dioxus applications.
4//! It requires `dioxus_provider::global::init_global_providers()` to be called at application startup.
5//!
6//! ## Example
7//!
8//! ```rust
9//! use dioxus::prelude::*;
10//! use dioxus_provider::{prelude::*, global::init_global_providers};
11//!
12//! #[provider]
13//! async fn fetch_user(id: u32) -> Result<String, String> {
14//!     Ok(format!("User {}", id))
15//! }
16//!
17//! #[component]
18//! fn App() -> Element {
19//!     let user = use_provider(fetch_user(), (1,));
20//!     rsx! { div { "User: {user:?}" } }
21//! }
22//!
23//! fn main() {
24//!     init_global_providers();
25//!     launch(App);
26//! }
27//! ```
28
29use dioxus::{
30    core::{ReactiveContext, SuspendedFuture},
31    prelude::*,
32};
33use std::{fmt::Debug, future::Future, time::Duration};
34
35
36use crate::{
37    cache::ProviderCache,
38    global::{get_global_cache, get_global_refresh_registry},
39    refresh::RefreshRegistry,
40};
41
42use crate::param_utils::IntoProviderParam;
43use crate::types::{ProviderErrorBounds, ProviderOutputBounds, ProviderParamBounds};
44
45// Import helper functions from internal modules
46use super::internal::cache_mgmt::setup_intelligent_cache_management;
47use super::internal::swr::check_and_handle_swr_core;
48use super::internal::tasks::{
49    check_and_handle_cache_expiration, setup_cache_expiration_task_core, setup_interval_task_core,
50    setup_stale_check_task_core,
51};
52
53pub use crate::provider_state::ProviderState;
54
55/// A unified trait for defining providers - async operations that return data
56///
57/// This trait supports both simple providers (no parameters) and parameterized providers.
58/// Use `Provider<()>` for simple providers and `Provider<ParamType>` for parameterized providers.
59///
60/// ## Features
61///
62/// - **Async Execution**: All providers are async by default
63/// - **Configurable Caching**: Optional cache expiration times
64/// - **Stale-While-Revalidate**: Serve stale data while revalidating in background
65/// - **Auto-Refresh**: Optional automatic refresh at intervals
66/// - **Auto-Dispose**: Automatic cleanup when providers are no longer used
67///
68/// ## Cross-Platform Compatibility
69///
70/// The Provider trait is designed to work across platforms using Dioxus's spawn system:
71/// - Uses `dioxus::spawn` for async execution (no Send + Sync required for most types)
72/// - Parameters may need Send + Sync if shared across contexts
73/// - Output and Error types only need Clone since they stay within Dioxus context
74///
75/// ## Example
76///
77/// ```rust,no_run
78/// use dioxus_provider::prelude::*;
79/// use std::time::Duration;
80///
81/// #[provider(stale_time = "1m", cache_expiration = "5m")]
82/// async fn data_provider() -> Result<String, String> {
83///     // Fetch data from API
84///     Ok("Hello, World!".to_string())
85/// }
86///
87/// #[component]
88/// fn Consumer() -> Element {
89///     let data = use_provider(data_provider(), ());
90///     // ...
91/// }
92/// ```
93pub trait Provider<Param = ()>: Clone + PartialEq + 'static
94where
95    Param: ProviderParamBounds,
96{
97    /// The type of data returned on success
98    type Output: ProviderOutputBounds;
99    /// The type of error returned on failure
100    type Error: ProviderErrorBounds;
101
102    /// Execute the async operation
103    ///
104    /// This method performs the actual work of the provider, such as fetching data
105    /// from an API, reading from a database, or computing a value.
106    fn run(&self, param: Param) -> impl Future<Output = Result<Self::Output, Self::Error>>;
107
108    /// Get a unique identifier for this provider instance with the given parameters
109    ///
110    /// This ID is used for caching and invalidation. The default implementation
111    /// hashes the provider's type and parameters to generate a unique ID.
112    fn id(&self, param: &Param) -> String {
113        use std::collections::hash_map::DefaultHasher;
114        use std::hash::{Hash, Hasher};
115
116        let mut hasher = DefaultHasher::new();
117        std::any::TypeId::of::<Self>().hash(&mut hasher);
118        param.hash(&mut hasher);
119        format!("{:x}", hasher.finish())
120    }
121
122    /// Get the interval duration for automatic refresh (None means no interval)
123    ///
124    /// When set, the provider will automatically refresh its data at the specified
125    /// interval, even if no component is actively watching it.
126    fn interval(&self) -> Option<Duration> {
127        None
128    }
129
130    /// Get the cache expiration duration (None means no expiration)
131    ///
132    /// When set, cached data will be considered expired after this duration and
133    /// will be removed from the cache, forcing a fresh fetch on the next access.
134    fn cache_expiration(&self) -> Option<Duration> {
135        None
136    }
137
138    /// Get the stale time duration for stale-while-revalidate behavior (None means no SWR)
139    ///
140    /// When set, data older than this duration will be considered stale and will
141    /// trigger a background revalidation while still serving the stale data to the UI.
142    fn stale_time(&self) -> Option<Duration> {
143        None
144    }
145}
146
147/// Extension trait to enable suspense support for provider signals
148///
149/// Allows you to call `.suspend()` on a `Signal<ProviderState<T, E>>`
150/// inside a component. If the state is `Loading`, this will suspend
151/// rendering and trigger Dioxus's SuspenseBoundary fallback.
152///
153/// Usage:
154/// ```rust
155/// let user = use_provider(fetch_user(), (1,)).suspend()?;
156/// ```
157pub trait SuspenseSignalExt<T, E> {
158    /// Returns Ok(data) if ready, Err(RenderError::Suspended) if loading, or Ok(Err(error)) if error.
159    fn suspend(&self) -> Result<Result<T, E>, RenderError>;
160}
161
162/// Error type for suspending rendering (compatible with Dioxus SuspenseBoundary)
163#[derive(Debug, Clone, PartialEq)]
164pub enum RenderError {
165    Suspended(SuspendedFuture),
166}
167
168// Implement conversion so `?` works in components using Dioxus's RenderError
169impl From<RenderError> for dioxus_core::RenderError {
170    fn from(err: RenderError) -> Self {
171        match err {
172            RenderError::Suspended(fut) => dioxus_core::RenderError::Suspended(fut),
173        }
174    }
175}
176
177// Update SuspenseSignalExt to use ProviderState
178impl<T: Clone + 'static, E: Clone + 'static> SuspenseSignalExt<T, E>
179    for Signal<ProviderState<T, E>>
180{
181    fn suspend(&self) -> Result<Result<T, E>, RenderError> {
182        match &*self.read() {
183            ProviderState::Loading { task } => {
184                Err(RenderError::Suspended(SuspendedFuture::new(*task)))
185            }
186            ProviderState::Success(data) => Ok(Ok(data.clone())),
187            ProviderState::Error(error) => Ok(Err(error.clone())),
188        }
189    }
190}
191
192/// Get the provider cache - requires global providers to be initialized
193fn get_provider_cache() -> ProviderCache {
194    get_global_cache()
195        .unwrap_or_else(|_| {
196            panic!("Global providers not initialized. Call dioxus_provider::init() before using providers.")
197        })
198        .clone()
199}
200
201/// Get the refresh registry - requires global providers to be initialized
202fn get_refresh_registry() -> RefreshRegistry {
203    get_global_refresh_registry()
204        .unwrap_or_else(|_| {
205            panic!("Global providers not initialized. Call dioxus_provider::init() before using providers.")
206        })
207        .clone()
208}
209
210/// Hook to access the provider cache for manual cache management
211///
212/// This hook provides direct access to the global provider cache for manual
213/// invalidation, clearing, and other cache operations.
214///
215/// ## Global Providers Required
216///
217/// You must call `init_global_providers()` at application startup before using any provider hooks.
218///
219/// ## Setup
220///
221/// ```rust,no_run
222/// use dioxus_provider::{prelude::*, global::init_global_providers};
223///
224/// fn main() {
225///     init_global_providers();
226///     dioxus::launch(App);
227/// }
228///
229/// #[component]
230/// fn App() -> Element {
231///     rsx! {
232///         MyComponent {}
233///     }
234/// }
235/// ```
236///
237/// ## Example
238///
239/// ```rust,no_run
240/// use dioxus::prelude::*;
241/// use dioxus_provider::prelude::*;
242///
243/// #[component]
244/// fn MyComponent() -> Element {
245///     let cache = use_provider_cache();
246///
247///     // Manually invalidate a specific cache entry
248///     cache.invalidate("my_provider_key");
249///
250///     rsx! {
251///         div { "Cache operations example" }
252///     }
253/// }
254/// ```
255pub fn use_provider_cache() -> ProviderCache {
256    get_provider_cache()
257}
258
259/// Hook to invalidate a specific provider cache entry
260///
261/// Returns a function that, when called, will invalidate the cache entry for the
262/// specified provider and parameters, and trigger a refresh of all components
263/// using that provider.
264///
265/// Requires global providers to be initialized with `init_global_providers()`.
266///
267/// ## Example
268///
269/// ```rust,no_run
270/// use dioxus::prelude::*;
271/// use dioxus_provider::prelude::*;
272///
273/// #[provider]
274/// async fn user_provider(id: u32) -> Result<String, String> {
275///     Ok(format!("User {}", id))
276/// }
277///
278/// #[component]
279/// fn MyComponent() -> Element {
280///     let invalidate_user = use_invalidate_provider(user_provider(), 1);
281///
282///     rsx! {
283///         button {
284///             onclick: move |_| invalidate_user(),
285///             "Refresh User Data"
286///         }
287///     }
288/// }
289/// ```
290pub fn use_invalidate_provider<P, Param>(provider: P, param: Param) -> impl Fn() + Clone
291where
292    P: Provider<Param>,
293    Param: ProviderParamBounds,
294{
295    let cache = get_provider_cache();
296    let refresh_registry = get_refresh_registry();
297    let cache_key = provider.id(&param);
298
299    move || {
300        cache.invalidate(&cache_key);
301        refresh_registry.trigger_refresh(&cache_key);
302    }
303}
304
305/// Hook to clear the entire provider cache
306///
307/// Returns a function that, when called, will clear all cached provider data
308/// and trigger a refresh of all providers currently in use.
309///
310/// Requires global providers to be initialized with `init_global_providers()`.
311///
312/// ## Example
313///
314/// ```rust,no_run
315/// use dioxus::prelude::*;
316/// use dioxus_provider::prelude::*;
317///
318/// #[component]
319/// fn MyComponent() -> Element {
320///     let clear_cache = use_clear_provider_cache();
321///
322///     rsx! {
323///         button {
324///             onclick: move |_| clear_cache(),
325///             "Clear All Cache"
326///         }
327///     }
328/// }
329/// ```
330pub fn use_clear_provider_cache() -> impl Fn() + Clone {
331    let cache = get_provider_cache();
332    let refresh_registry = get_refresh_registry();
333
334    move || {
335        cache.clear();
336        refresh_registry.clear_all();
337    }
338}
339
340/// Unified trait for using providers with any parameter format
341///
342/// This trait provides a single, unified interface for using providers
343/// regardless of their parameter format. It automatically handles:
344/// - No parameters `()`
345/// - Tuple parameters `(param,)`
346/// - Direct parameters `param`
347pub trait UseProvider<Args> {
348    /// The type of data returned on success
349    type Output: ProviderOutputBounds;
350    /// The type of error returned on failure
351    type Error: ProviderErrorBounds;
352
353    /// Use the provider with the given arguments
354    fn use_provider(self, args: Args) -> Signal<ProviderState<Self::Output, Self::Error>>;
355}
356
357/// Unified implementation for all providers using parameter normalization
358///
359/// This single implementation replaces all the previous repetitive implementations
360/// by using the `IntoProviderParam` trait to normalize different parameter formats.
361impl<P, Args> UseProvider<Args> for P
362where
363    P: Provider<Args::Param> + Send + Clone,
364    Args: IntoProviderParam,
365{
366    type Output = P::Output;
367    type Error = P::Error;
368
369    fn use_provider(self, args: Args) -> Signal<ProviderState<Self::Output, Self::Error>> {
370        let param = args.into_param();
371        use_provider_core(self, param)
372    }
373}
374
375/// Core provider implementation that handles all the common logic
376fn use_provider_core<P, Param>(
377    provider: P,
378    param: Param,
379) -> Signal<ProviderState<P::Output, P::Error>>
380where
381    P: Provider<Param> + Send + Clone,
382    Param: ProviderParamBounds,
383{
384    let mut state = use_signal(|| ProviderState::Loading {
385        task: spawn(async {}),
386    });
387    let cache = get_provider_cache();
388    let refresh_registry = get_refresh_registry();
389
390    let cache_key = provider.id(&param);
391    let cache_expiration = provider.cache_expiration();
392
393    // Setup intelligent cache management (replaces old auto-dispose system)
394    setup_intelligent_cache_management(&provider, &cache_key, &cache, &refresh_registry);
395
396    // Check cache expiration before the memo - this happens on every render
397    check_and_handle_cache_expiration(cache_expiration, &cache_key, &cache, &refresh_registry);
398
399    // SWR staleness checking - runs on every render to check for stale data
400    check_and_handle_swr_core(&provider, &param, &cache_key, &cache, &refresh_registry);
401
402    // Use memo with reactive dependencies to track changes automatically
403    let _execution_memo = use_memo(use_reactive!(|(provider, param)| {
404        let cache_key = provider.id(&param);
405
406        #[cfg(feature = "tracing")]
407        crate::debug_log!(
408            "🔄 [USE_PROVIDER] Memo executing for key: {} with param: {:?}",
409            cache_key, param
410        );
411
412        // Subscribe to refresh events for this cache key if we have a reactive context
413        if let Some(reactive_context) = ReactiveContext::current() {
414            refresh_registry.subscribe_to_refresh(&cache_key, reactive_context);
415        }
416
417        // Read the current refresh count (this makes the memo reactive to changes)
418        let _current_refresh_count = refresh_registry.get_refresh_count(&cache_key);
419
420        // Set up cache expiration monitoring task
421        setup_cache_expiration_task_core(&provider, &param, &cache_key, &cache, &refresh_registry);
422
423        // Set up interval task if provider has interval configured
424        setup_interval_task_core(&provider, &param, &cache_key, &cache, &refresh_registry);
425
426        // Set up stale check task if provider has stale time configured
427        setup_stale_check_task_core(&provider, &param, &cache_key, &cache, &refresh_registry);
428
429        // Check cache for valid data
430        if let Some(cached_result) = cache.get::<Result<P::Output, P::Error>>(&cache_key) {
431            // Access tracking is automatically handled by cache.get() updating last_accessed time
432            crate::debug_log!("📊 [CACHE-HIT] Serving cached data for: {}", cache_key);
433
434            match cached_result {
435                Ok(data) => {
436                    let _ = spawn(async move {
437                        state.set(ProviderState::Success(data));
438                    });
439                }
440                Err(error) => {
441                    let _ = spawn(async move {
442                        state.set(ProviderState::Error(error));
443                    });
444                }
445            }
446            return;
447        }
448
449        // Cache miss - check if this is due to invalidation and we should use SWR behavior
450        let is_invalidation_refresh = refresh_registry.get_refresh_count(&cache_key) > 0;
451        
452        if is_invalidation_refresh {
453            // This is an invalidation refresh - use SWR behavior to prevent jitters
454            // Don't show loading state immediately, let SWR handle background revalidation
455            crate::debug_log!("🔄 [INVALIDATION] Cache miss due to invalidation for: {}, using SWR behavior", cache_key);
456            
457            // Set up background revalidation without showing loading state
458            let cache_clone = cache.clone();
459            let cache_key_clone = cache_key.clone();
460            let provider = provider.clone();
461            let param = param.clone();
462            let refresh_registry_clone = refresh_registry.clone();
463            
464            spawn(async move {
465                let result = provider.run(param).await;
466                let updated = cache_clone.set(cache_key_clone.clone(), result.clone());
467                if updated {
468                    refresh_registry_clone.trigger_refresh(&cache_key_clone);
469                    crate::debug_log!("✅ [INVALIDATION] Background revalidation completed for: {}", cache_key_clone);
470                }
471            });
472            
473            // Don't set loading state - let the component handle the absence of data gracefully
474            return;
475        }
476
477        // Regular cache miss - set loading and spawn async task
478        let cache_clone = cache.clone();
479        let cache_key_clone = cache_key.clone();
480        let provider = provider.clone();
481        let param = param.clone();
482        let mut state_for_async = state;
483
484        // Spawn the real async task and store the handle in Loading
485        let task = spawn(async move {
486            let result = provider.run(param).await;
487            let updated = cache_clone.set(cache_key_clone.clone(), result.clone());
488            crate::debug_log!(
489                "📊 [CACHE-STORE] Attempted to store new data for: {} (updated: {})",
490                cache_key_clone, updated
491            );
492            if updated {
493                // Only update state and trigger rerender if value changed
494                match result {
495                    Ok(data) => state_for_async.set(ProviderState::Success(data)),
496                    Err(error) => state_for_async.set(ProviderState::Error(error)),
497                }
498            }
499        });
500        state.set(ProviderState::Loading { task });
501    }));
502
503    state
504}
505
506/// Performs SWR staleness checking and triggers background revalidation if needed
507/// Unified hook for using any provider - automatically detects parameterized vs non-parameterized providers
508///
509/// This is the main hook for consuming providers in Dioxus components. It automatically
510/// handles both simple providers (no parameters) and parameterized providers, providing
511/// a consistent interface for all provider types through the `IntoProviderParam` trait.
512///
513/// ## Supported Parameter Formats
514///
515/// - **No parameters**: `use_provider(provider, ())`
516/// - **Tuple parameters**: `use_provider(provider, (param,))`
517/// - **Direct parameters**: `use_provider(provider, param)`
518///
519/// ## Features
520///
521/// - **Automatic Caching**: Results are cached based on provider configuration
522/// - **Reactive Updates**: Components automatically re-render when data changes
523/// - **Loading States**: Provides loading, success, and error states
524/// - **Background Refresh**: Supports interval refresh and stale-while-revalidate
525/// - **Auto-Dispose**: Automatically cleans up unused providers
526/// - **Unified API**: Single function handles all parameter formats
527///
528/// ## Usage Examples
529///
530/// ```rust,no_run
531/// use dioxus::prelude::*;
532/// use dioxus_provider::prelude::*;
533///
534/// #[provider]
535/// async fn fetch_user() -> Result<String, String> {
536///     Ok("User data".to_string())
537/// }
538///
539/// #[provider]
540/// async fn fetch_user_by_id(user_id: u32) -> Result<String, String> {
541///     Ok(format!("User {}", user_id))
542/// }
543///
544/// #[component]
545/// fn MyComponent() -> Element {
546///     // All of these work seamlessly:
547///     let user = use_provider(fetch_user(), ());           // No parameters
548///     let user_by_id = use_provider(fetch_user_by_id(), 123);     // Direct parameter
549///     let user_by_id_tuple = use_provider(fetch_user_by_id(), (123,)); // Tuple parameter
550///
551///     rsx! {
552///         div { "Users loaded!" }
553///     }
554/// }
555/// ```
556pub fn use_provider<P, Args>(provider: P, args: Args) -> Signal<ProviderState<P::Output, P::Error>>
557where
558    P: UseProvider<Args>,
559{
560    provider.use_provider(args)
561}