use crate::hooks::context::{Effect, current_context};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub trait Deps {
fn to_hash(&self) -> u64;
}
impl Deps for () {
fn to_hash(&self) -> u64 {
0
}
}
impl<T: Hash> Deps for (T,) {
fn to_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
hasher.finish()
}
}
impl<T1: Hash, T2: Hash> Deps for (T1, T2) {
fn to_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
self.1.hash(&mut hasher);
hasher.finish()
}
}
impl<T1: Hash, T2: Hash, T3: Hash> Deps for (T1, T2, T3) {
fn to_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
self.1.hash(&mut hasher);
self.2.hash(&mut hasher);
hasher.finish()
}
}
impl<T1: Hash, T2: Hash, T3: Hash, T4: Hash> Deps for (T1, T2, T3, T4) {
fn to_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.0.hash(&mut hasher);
self.1.hash(&mut hasher);
self.2.hash(&mut hasher);
self.3.hash(&mut hasher);
hasher.finish()
}
}
impl<T: Hash> Deps for Vec<T> {
fn to_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
for item in self {
item.hash(&mut hasher);
}
hasher.finish()
}
}
#[derive(Clone)]
struct EffectStorage {
prev_deps_hash: Option<u64>,
}
pub fn use_effect<F, D>(effect: F, deps: D)
where
F: FnOnce() -> Option<Box<dyn FnOnce() + Send>> + Send + 'static,
D: Deps + 'static,
{
let ctx = current_context().expect("use_effect must be called within a component");
let mut ctx_ref = ctx.write().unwrap();
let new_deps_hash = deps.to_hash();
let storage = ctx_ref.use_hook(|| EffectStorage {
prev_deps_hash: None,
});
let prev_deps_hash = storage
.get::<EffectStorage>()
.and_then(|s| s.prev_deps_hash);
let should_run = match prev_deps_hash {
None => true, Some(prev) => prev != new_deps_hash, };
if should_run {
storage.set(EffectStorage {
prev_deps_hash: Some(new_deps_hash),
});
ctx_ref.add_effect(Effect {
callback: Box::new(effect),
cleanup: None,
deps: Some(vec![new_deps_hash]),
});
}
}
pub fn use_effect_once<F>(effect: F)
where
F: FnOnce() -> Option<Box<dyn FnOnce() + Send>> + Send + 'static,
{
let ctx = current_context().expect("use_effect_once must be called within a component");
let mut ctx_ref = ctx.write().unwrap();
let storage = ctx_ref.use_hook(|| false);
let has_run = storage.get::<bool>().unwrap_or(false);
if !has_run {
storage.set(true);
ctx_ref.add_effect(Effect {
callback: Box::new(effect),
cleanup: None,
deps: None,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hooks::context::{HookContext, with_hooks};
use std::sync::{Arc, Mutex, RwLock};
#[test]
fn test_deps_hash() {
let deps1 = (1i32, 2i32);
let deps2 = (1i32, 2i32);
let deps3 = (1i32, 3i32);
assert_eq!(deps1.to_hash(), deps2.to_hash());
assert_ne!(deps1.to_hash(), deps3.to_hash());
}
#[test]
fn test_use_effect_runs() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let effect_ran = Arc::new(Mutex::new(false));
let effect_ran_clone = effect_ran.clone();
with_hooks(ctx.clone(), || {
use_effect(
move || {
*effect_ran_clone.lock().unwrap() = true;
None
},
(),
);
});
assert!(*effect_ran.lock().unwrap());
}
#[test]
fn test_use_effect_with_deps() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let run_count = Arc::new(Mutex::new(0));
let run_count_clone = run_count.clone();
with_hooks(ctx.clone(), || {
use_effect(
move || {
*run_count_clone.lock().unwrap() += 1;
None
},
(1i32,),
);
});
assert_eq!(*run_count.lock().unwrap(), 1);
let run_count_clone = run_count.clone();
with_hooks(ctx.clone(), || {
use_effect(
move || {
*run_count_clone.lock().unwrap() += 1;
None
},
(1i32,),
);
});
assert_eq!(*run_count.lock().unwrap(), 1);
let run_count_clone = run_count.clone();
with_hooks(ctx.clone(), || {
use_effect(
move || {
*run_count_clone.lock().unwrap() += 1;
None
},
(2i32,),
);
});
assert_eq!(*run_count.lock().unwrap(), 2);
}
#[test]
fn test_use_effect_cleanup() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let cleanup_ran = Arc::new(Mutex::new(false));
let cleanup_ran_clone = cleanup_ran.clone();
with_hooks(ctx.clone(), || {
use_effect(
move || {
Some(Box::new(move || {
*cleanup_ran_clone.lock().unwrap() = true;
}) as Box<dyn FnOnce() + Send>)
},
(1i32,),
);
});
assert!(!*cleanup_ran.lock().unwrap());
with_hooks(ctx.clone(), || {
use_effect(|| None, (2i32,));
});
assert!(*cleanup_ran.lock().unwrap());
}
}