wasm_react/
component.rs

1use crate::{react_bindings, VNode};
2use std::any::{type_name, Any};
3use js_sys::JsString;
4use wasm_bindgen::prelude::*;
5
6/// Implemented by types which can serve as a [React key][key].
7///
8/// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key
9pub trait KeyType: Into<JsValue> {}
10
11macro_rules! impl_key_type {
12  { $( $T:ty ),* $( , )? } => {
13    $( impl KeyType for $T {} )*
14  };
15}
16
17impl_key_type! {
18  &str, String, JsString,
19  f32, f64,
20  i8, i16, i32, i64, i128, isize,
21  u8, u16, u32, u64, u128, usize,
22}
23
24#[doc(hidden)]
25pub struct BuildParams {
26  name: &'static str,
27  key: Option<JsValue>,
28}
29
30/// Implement this trait on a struct to create a component with the struct as
31/// props.
32///
33/// The props will be completely controlled by Rust, which makes rendering them
34/// relatively simple in Rust. However, since the props struct cannot be
35/// constructed in JS, these components cannot be exposed to JS. This means only
36/// components written in Rust can render a `Component` by default.
37///
38/// See [`export_components!`](crate::export_components!) for how to expose
39/// components for JS consumption.
40///
41/// # Example
42///
43/// ```
44/// # use wasm_react::*;
45/// struct Counter(i32);
46///
47/// impl Component for Counter {
48///   fn render(&self) -> VNode {
49///     h!(div).build(("Counter: ", self.0))
50///   }
51/// }
52/// ```
53pub trait Component: Sized + 'static {
54  /// The render function.
55  ///
56  /// **Do not** call this method in another render function. Instead, use
57  /// [`Component::build()`] to include your component.
58  fn render(&self) -> VNode;
59
60  /// Sets the [React key][key].
61  ///
62  /// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key
63  fn key(self, key: Option<impl KeyType>) -> Keyed<Self> {
64    Keyed(self, key.map(|x| x.into()))
65  }
66
67  #[doc(hidden)]
68  /// Defines parameters for [`Component::build()`].
69  fn _build_params(&self) -> BuildParams {
70    BuildParams {
71      name: type_name::<Self>(),
72      key: None,
73    }
74  }
75
76  #[doc(hidden)]
77  fn _build_with_name_and_key(
78    self,
79    name: &'static str,
80    key: Option<JsValue>,
81  ) -> VNode {
82    VNode::Single(react_bindings::create_rust_component(
83      name,
84      &key.unwrap_or(JsValue::UNDEFINED),
85      ComponentWrapper(Box::new(self)),
86    ))
87  }
88
89  /// Returns a [`VNode`] to be included in a render function.
90  fn build(self) -> VNode {
91    let BuildParams { name, key } = self._build_params();
92
93    self._build_with_name_and_key(name, key)
94  }
95
96  /// Returns a memoized version of your component that skips rendering if props
97  /// haven't changed.
98  ///
99  /// If your component renders the same result given the same props, you can
100  /// memoize your component for a performance boost.
101  ///
102  /// You have to implement [`PartialEq`] on your [`Component`] for this to work.
103  ///
104  /// # Example
105  ///
106  /// ```
107  /// # use std::rc::Rc;
108  /// # use wasm_react::*;
109  /// #[derive(PartialEq)]
110  /// struct MessageBox {
111  ///   message: Rc<str>,
112  /// }
113  ///
114  /// impl Component for MessageBox {
115  ///   fn render(&self) -> VNode {
116  ///     h!(h1[."message-box"]).build(&*self.message)
117  ///   }
118  /// }
119  ///
120  /// struct App;
121  ///
122  /// impl Component for App {
123  ///   fn render(&self) -> VNode {
124  ///     h!(div[#"app"]).build(
125  ///       MessageBox {
126  ///         message: Rc::from("Hello World!"),
127  ///       }
128  ///       .memoized()
129  ///       .build()
130  ///     )
131  ///   }
132  /// }
133  /// ```
134  fn memoized(self) -> Memoized<Self>
135  where
136    Self: PartialEq,
137  {
138    Memoized(self)
139  }
140}
141
142/// Wraps your component to assign a [React key][key] to it.
143///
144/// See [`Component::key()`].
145///
146/// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key
147#[derive(Debug, PartialEq)]
148pub struct Keyed<T>(T, Option<JsValue>);
149
150impl<T: Component> Component for Keyed<T> {
151  fn render(&self) -> VNode {
152    self.0.render()
153  }
154
155  fn _build_params(&self) -> BuildParams {
156    let BuildParams { name, .. } = self.0._build_params();
157
158    BuildParams {
159      name,
160      key: self.1.clone(),
161    }
162  }
163
164  fn _build_with_name_and_key(
165    self,
166    name: &'static str,
167    key: Option<JsValue>,
168  ) -> VNode {
169    self.0._build_with_name_and_key(name, key)
170  }
171}
172
173/// Wraps your component to let React skip rendering if props haven't changed.
174///
175/// See [`Component::memoized()`].
176#[derive(Debug, PartialEq)]
177pub struct Memoized<T>(T);
178
179impl<T: Component + PartialEq> Component for Memoized<T> {
180  fn render(&self) -> VNode {
181    self.0.render()
182  }
183
184  fn _build_params(&self) -> BuildParams {
185    self.0._build_params()
186  }
187
188  fn _build_with_name_and_key(
189    self,
190    name: &'static str,
191    key: Option<JsValue>,
192  ) -> VNode {
193    VNode::Single(react_bindings::create_rust_memo_component(
194      name,
195      &key.unwrap_or(JsValue::UNDEFINED),
196      MemoComponentWrapper(Box::new(self.0)),
197    ))
198  }
199}
200
201trait ObjectSafeComponent {
202  fn render(&self) -> VNode;
203}
204
205impl<T: Component> ObjectSafeComponent for T {
206  fn render(&self) -> VNode {
207    Component::render(self)
208  }
209}
210
211#[doc(hidden)]
212#[wasm_bindgen(js_name = __WasmReact_ComponentWrapper)]
213pub struct ComponentWrapper(Box<dyn ObjectSafeComponent>);
214
215#[wasm_bindgen(js_class = __WasmReact_ComponentWrapper)]
216impl ComponentWrapper {
217  #[wasm_bindgen]
218  pub fn render(&self) -> JsValue {
219    self.0.render().into()
220  }
221}
222
223trait ObjectSafeMemoComponent: ObjectSafeComponent {
224  fn as_any(&self) -> &dyn Any;
225  fn eq(&self, other: &dyn Any) -> bool;
226}
227
228impl<T: Component + PartialEq> ObjectSafeMemoComponent for T {
229  fn as_any(&self) -> &dyn Any {
230    self
231  }
232
233  fn eq(&self, other: &dyn Any) -> bool {
234    other
235      .downcast_ref::<T>()
236      .map(|other| PartialEq::eq(self, other))
237      .unwrap_or(false)
238  }
239}
240
241#[doc(hidden)]
242#[wasm_bindgen(js_name = __WasmReact_MemoComponentWrapper)]
243pub struct MemoComponentWrapper(Box<dyn ObjectSafeMemoComponent>);
244
245#[wasm_bindgen(js_class = __WasmReact_MemoComponentWrapper)]
246impl MemoComponentWrapper {
247  #[wasm_bindgen]
248  pub fn render(&self) -> JsValue {
249    self.0.render().into()
250  }
251
252  #[wasm_bindgen]
253  pub fn eq(&self, other: &MemoComponentWrapper) -> bool {
254    self.0.eq(other.0.as_any())
255  }
256}