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(¶m);
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(¶m);
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, ¶m, &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(¶m);
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, ¶m, &cache_key, &cache, &refresh_registry);
422
423 // Set up interval task if provider has interval configured
424 setup_interval_task_core(&provider, ¶m, &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, ¶m, &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}