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