1use std::any::Any;
24use std::cell::RefCell;
25use std::fmt;
26use std::rc::Rc;
27
28use wasm_bindgen::prelude::*;
29
30#[cfg(all(feature = "hydration", feature = "ssr"))]
31use crate::html::RenderMode;
32use crate::html::{AnyScope, BaseComponent, Context, HtmlResult};
33use crate::Properties;
34
35mod hooks;
36pub use hooks::*;
37pub use yew_macro::function_component;
64pub use yew_macro::hook;
66
67type ReRender = Rc<dyn Fn()>;
68
69#[cfg(any(feature = "hydration", feature = "ssr"))]
71pub(crate) trait PreparedState {
72 #[cfg(feature = "ssr")]
73 fn prepare(&self) -> String;
74}
75
76pub(crate) trait Effect {
78 fn rendered(&self) {}
79}
80
81pub struct HookContext {
83 pub(crate) scope: AnyScope,
84 #[cfg(all(feature = "hydration", feature = "ssr"))]
85 creation_mode: RenderMode,
86 re_render: ReRender,
87
88 states: Vec<Rc<dyn Any>>,
89 effects: Vec<Rc<dyn Effect>>,
90
91 #[cfg(any(feature = "hydration", feature = "ssr"))]
92 prepared_states: Vec<Rc<dyn PreparedState>>,
93
94 #[cfg(feature = "hydration")]
95 prepared_states_data: Vec<Rc<str>>,
96 #[cfg(feature = "hydration")]
97 prepared_state_counter: usize,
98
99 counter: usize,
100 #[cfg(debug_assertions)]
101 total_hook_counter: Option<usize>,
102}
103
104impl HookContext {
105 fn new(
106 scope: AnyScope,
107 re_render: ReRender,
108 #[cfg(all(feature = "hydration", feature = "ssr"))] creation_mode: RenderMode,
109 #[cfg(feature = "hydration")] prepared_state: Option<&str>,
110 ) -> RefCell<Self> {
111 RefCell::new(HookContext {
112 scope,
113 re_render,
114
115 #[cfg(all(feature = "hydration", feature = "ssr"))]
116 creation_mode,
117
118 states: Vec::new(),
119
120 #[cfg(any(feature = "hydration", feature = "ssr"))]
121 prepared_states: Vec::new(),
122 effects: Vec::new(),
123
124 #[cfg(feature = "hydration")]
125 prepared_states_data: {
126 match prepared_state {
127 Some(m) => m.split(',').map(Rc::from).collect(),
128 None => Vec::new(),
129 }
130 },
131 #[cfg(feature = "hydration")]
132 prepared_state_counter: 0,
133
134 counter: 0,
135 #[cfg(debug_assertions)]
136 total_hook_counter: None,
137 })
138 }
139
140 pub(crate) fn next_state<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
141 where
142 T: 'static,
143 {
144 let hook_pos = self.counter;
146 self.counter += 1;
147
148 let state = match self.states.get(hook_pos).cloned() {
149 Some(m) => m,
150 None => {
151 let initial_state = Rc::new(initializer(self.re_render.clone()));
152 self.states.push(initial_state.clone());
153
154 initial_state
155 }
156 };
157
158 state.downcast().unwrap_throw()
159 }
160
161 pub(crate) fn next_effect<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
162 where
163 T: 'static + Effect,
164 {
165 let prev_state_len = self.states.len();
166 let t = self.next_state(initializer);
167
168 if self.states.len() != prev_state_len {
170 self.effects.push(t.clone());
171 }
172
173 t
174 }
175
176 #[cfg(any(feature = "hydration", feature = "ssr"))]
177 pub(crate) fn next_prepared_state<T>(
178 &mut self,
179 initializer: impl FnOnce(ReRender, Option<&str>) -> T,
180 ) -> Rc<T>
181 where
182 T: 'static + PreparedState,
183 {
184 #[cfg(not(feature = "hydration"))]
185 let prepared_state = Option::<Rc<str>>::None;
186
187 #[cfg(feature = "hydration")]
188 let prepared_state = {
189 let prepared_state_pos = self.prepared_state_counter;
190 self.prepared_state_counter += 1;
191
192 self.prepared_states_data.get(prepared_state_pos).cloned()
193 };
194
195 let prev_state_len = self.states.len();
196 let t = self.next_state(move |re_render| initializer(re_render, prepared_state.as_deref()));
197
198 if self.states.len() != prev_state_len {
200 self.prepared_states.push(t.clone());
201 }
202
203 t
204 }
205
206 #[inline(always)]
207 fn prepare_run(&mut self) {
208 #[cfg(feature = "hydration")]
209 {
210 self.prepared_state_counter = 0;
211 }
212
213 self.counter = 0;
214 }
215
216 #[cfg(debug_assertions)]
220 fn assert_hook_context(&mut self, render_ok: bool) {
221 match (render_ok, self.total_hook_counter) {
224 (true, None) => {
227 self.total_hook_counter = Some(self.counter);
228 }
229 (false, None) => {}
232
233 (true, Some(total_hook_counter)) => assert_eq!(
236 total_hook_counter, self.counter,
237 "Hooks are called conditionally."
238 ),
239
240 (false, Some(total_hook_counter)) => assert!(
243 self.counter <= total_hook_counter,
244 "Hooks are called conditionally."
245 ),
246 }
247 }
248
249 fn run_effects(&self) {
250 for effect in self.effects.iter() {
251 effect.rendered();
252 }
253 }
254
255 fn drain_states(&mut self) {
256 self.effects.clear();
258
259 for state in self.states.drain(..) {
260 drop(state);
261 }
262 }
263
264 #[cfg(not(feature = "ssr"))]
265 fn prepare_state(&self) -> Option<String> {
266 None
267 }
268
269 #[cfg(feature = "ssr")]
270 fn prepare_state(&self) -> Option<String> {
271 if self.prepared_states.is_empty() {
272 return None;
273 }
274
275 let prepared_states = self.prepared_states.clone();
276
277 let mut states = Vec::new();
278
279 for state in prepared_states.iter() {
280 let state = state.prepare();
281 states.push(state);
282 }
283
284 Some(states.join(","))
285 }
286}
287
288impl fmt::Debug for HookContext {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 f.write_str("HookContext<_>")
291 }
292}
293
294pub trait FunctionProvider {
296 type Properties: Properties + PartialEq;
298
299 fn run(ctx: &mut HookContext, props: &Self::Properties) -> HtmlResult;
304}
305
306#[doc(hidden)]
315pub struct FunctionComponent<T>
316where
317 T: FunctionProvider,
318{
319 _never: std::marker::PhantomData<T>,
320 hook_ctx: RefCell<HookContext>,
321}
322
323impl<T> FunctionComponent<T>
324where
325 T: FunctionProvider + 'static,
326{
327 pub fn new(ctx: &Context<T>) -> Self
329 where
330 T: BaseComponent<Message = ()> + FunctionProvider + 'static,
331 {
332 let scope = AnyScope::from(ctx.link().clone());
333 let re_render = {
334 let link = ctx.link().clone();
335
336 Rc::new(move || link.send_message(()))
337 };
338
339 Self {
340 _never: std::marker::PhantomData,
341 hook_ctx: HookContext::new(
342 scope,
343 re_render,
344 #[cfg(all(feature = "hydration", feature = "ssr"))]
345 ctx.creation_mode(),
346 #[cfg(feature = "hydration")]
347 ctx.prepared_state(),
348 ),
349 }
350 }
351
352 pub fn render(&self, props: &T::Properties) -> HtmlResult {
354 let mut hook_ctx = self.hook_ctx.borrow_mut();
355
356 hook_ctx.prepare_run();
357
358 #[allow(clippy::let_and_return)]
359 let result = T::run(&mut hook_ctx, props);
360
361 #[cfg(debug_assertions)]
362 hook_ctx.assert_hook_context(result.is_ok());
363
364 result
365 }
366
367 pub fn rendered(&self) {
369 let hook_ctx = self.hook_ctx.borrow();
370 hook_ctx.run_effects();
371 }
372
373 pub fn destroy(&self) {
375 let mut hook_ctx = self.hook_ctx.borrow_mut();
376 hook_ctx.drain_states();
377 }
378
379 pub fn prepare_state(&self) -> Option<String> {
381 let hook_ctx = self.hook_ctx.borrow();
382 hook_ctx.prepare_state()
383 }
384}
385
386impl<T> fmt::Debug for FunctionComponent<T>
387where
388 T: FunctionProvider + 'static,
389{
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 f.write_str("FunctionComponent<_>")
392 }
393}