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