use std::any::Any;
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
#[cfg(all(feature = "hydration", feature = "ssr"))]
use crate::html::RenderMode;
use crate::html::{AnyScope, BaseComponent, Context, HtmlResult};
use crate::Properties;
mod hooks;
pub use hooks::*;
pub use yew_macro::function_component;
pub use yew_macro::hook;
type ReRender = Rc<dyn Fn()>;
#[cfg(any(feature = "hydration", feature = "ssr"))]
pub(crate) trait PreparedState {
#[cfg(feature = "ssr")]
fn prepare(&self) -> String;
}
pub(crate) trait Effect {
fn rendered(&self) {}
}
pub struct HookContext {
pub(crate) scope: AnyScope,
#[cfg(all(feature = "hydration", feature = "ssr"))]
creation_mode: RenderMode,
re_render: ReRender,
states: Vec<Rc<dyn Any>>,
effects: Vec<Rc<dyn Effect>>,
#[cfg(any(feature = "hydration", feature = "ssr"))]
prepared_states: Vec<Rc<dyn PreparedState>>,
#[cfg(feature = "hydration")]
prepared_states_data: Vec<Rc<str>>,
#[cfg(feature = "hydration")]
prepared_state_counter: usize,
counter: usize,
#[cfg(debug_assertions)]
total_hook_counter: Option<usize>,
}
impl HookContext {
fn new(
scope: AnyScope,
re_render: ReRender,
#[cfg(all(feature = "hydration", feature = "ssr"))] creation_mode: RenderMode,
#[cfg(feature = "hydration")] prepared_state: Option<&str>,
) -> RefCell<Self> {
RefCell::new(HookContext {
scope,
re_render,
#[cfg(all(feature = "hydration", feature = "ssr"))]
creation_mode,
states: Vec::new(),
#[cfg(any(feature = "hydration", feature = "ssr"))]
prepared_states: Vec::new(),
effects: Vec::new(),
#[cfg(feature = "hydration")]
prepared_states_data: {
match prepared_state {
Some(m) => m.split(',').map(Rc::from).collect(),
None => Vec::new(),
}
},
#[cfg(feature = "hydration")]
prepared_state_counter: 0,
counter: 0,
#[cfg(debug_assertions)]
total_hook_counter: None,
})
}
pub(crate) fn next_state<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
where
T: 'static,
{
let hook_pos = self.counter;
self.counter += 1;
let state = match self.states.get(hook_pos).cloned() {
Some(m) => m,
None => {
let initial_state = Rc::new(initializer(self.re_render.clone()));
self.states.push(initial_state.clone());
initial_state
}
};
state.downcast().unwrap_throw()
}
pub(crate) fn next_effect<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
where
T: 'static + Effect,
{
let prev_state_len = self.states.len();
let t = self.next_state(initializer);
if self.states.len() != prev_state_len {
self.effects.push(t.clone());
}
t
}
#[cfg(any(feature = "hydration", feature = "ssr"))]
pub(crate) fn next_prepared_state<T>(
&mut self,
initializer: impl FnOnce(ReRender, Option<&str>) -> T,
) -> Rc<T>
where
T: 'static + PreparedState,
{
#[cfg(not(feature = "hydration"))]
let prepared_state = Option::<Rc<str>>::None;
#[cfg(feature = "hydration")]
let prepared_state = {
let prepared_state_pos = self.prepared_state_counter;
self.prepared_state_counter += 1;
self.prepared_states_data.get(prepared_state_pos).cloned()
};
let prev_state_len = self.states.len();
let t = self.next_state(move |re_render| initializer(re_render, prepared_state.as_deref()));
if self.states.len() != prev_state_len {
self.prepared_states.push(t.clone());
}
t
}
#[inline(always)]
fn prepare_run(&mut self) {
#[cfg(feature = "hydration")]
{
self.prepared_state_counter = 0;
}
self.counter = 0;
}
#[cfg(debug_assertions)]
fn assert_hook_context(&mut self, render_ok: bool) {
match (render_ok, self.total_hook_counter) {
(true, None) => {
self.total_hook_counter = Some(self.counter);
}
(false, None) => {}
(true, Some(total_hook_counter)) => assert_eq!(
total_hook_counter, self.counter,
"Hooks are called conditionally."
),
(false, Some(total_hook_counter)) => assert!(
self.counter <= total_hook_counter,
"Hooks are called conditionally."
),
}
}
fn run_effects(&self) {
for effect in self.effects.iter() {
effect.rendered();
}
}
fn drain_states(&mut self) {
self.effects.clear();
for state in self.states.drain(..) {
drop(state);
}
}
#[cfg(not(feature = "ssr"))]
fn prepare_state(&self) -> Option<String> {
None
}
#[cfg(feature = "ssr")]
fn prepare_state(&self) -> Option<String> {
if self.prepared_states.is_empty() {
return None;
}
let prepared_states = self.prepared_states.clone();
let mut states = Vec::new();
for state in prepared_states.iter() {
let state = state.prepare();
states.push(state);
}
Some(states.join(","))
}
}
impl fmt::Debug for HookContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("HookContext<_>")
}
}
pub trait FunctionProvider {
type Properties: Properties + PartialEq;
fn run(ctx: &mut HookContext, props: &Self::Properties) -> HtmlResult;
}
#[doc(hidden)]
pub struct FunctionComponent<T>
where
T: FunctionProvider,
{
_never: std::marker::PhantomData<T>,
hook_ctx: RefCell<HookContext>,
}
impl<T> FunctionComponent<T>
where
T: FunctionProvider + 'static,
{
pub fn new(ctx: &Context<T>) -> Self
where
T: BaseComponent<Message = ()> + FunctionProvider + 'static,
{
let scope = AnyScope::from(ctx.link().clone());
let re_render = {
let link = ctx.link().clone();
Rc::new(move || link.send_message(()))
};
Self {
_never: std::marker::PhantomData::default(),
hook_ctx: HookContext::new(
scope,
re_render,
#[cfg(all(feature = "hydration", feature = "ssr"))]
ctx.creation_mode(),
#[cfg(feature = "hydration")]
ctx.prepared_state(),
),
}
}
pub fn render(&self, props: &T::Properties) -> HtmlResult {
let mut hook_ctx = self.hook_ctx.borrow_mut();
hook_ctx.prepare_run();
#[allow(clippy::let_and_return)]
let result = T::run(&mut hook_ctx, props);
#[cfg(debug_assertions)]
hook_ctx.assert_hook_context(result.is_ok());
result
}
pub fn rendered(&self) {
let hook_ctx = self.hook_ctx.borrow();
hook_ctx.run_effects();
}
pub fn destroy(&self) {
let mut hook_ctx = self.hook_ctx.borrow_mut();
hook_ctx.drain_states();
}
pub fn prepare_state(&self) -> Option<String> {
let hook_ctx = self.hook_ctx.borrow();
hook_ctx.prepare_state()
}
}
impl<T> fmt::Debug for FunctionComponent<T>
where
T: FunctionProvider + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("FunctionComponent<_>")
}
}