This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/
server_renderer.rs

1use std::fmt;
2use std::future::Future;
3
4use futures::pin_mut;
5use futures::stream::{Stream, StreamExt};
6use tracing::Instrument;
7
8use crate::html::{BaseComponent, Scope};
9use crate::platform::fmt::BufStream;
10use crate::platform::{LocalHandle, Runtime};
11
12#[cfg(feature = "ssr")]
13pub(crate) mod feat_ssr {
14    /// Passed top-down as context for `render_into_stream` functions to know the current innermost
15    /// `VTag` kind to apply appropriate text escaping.
16    /// Right now this is used to make `VText` nodes aware of their environment and correctly
17    /// escape their contents when rendering them during SSR.
18    #[derive(Default, Clone, Copy)]
19    pub(crate) enum VTagKind {
20        /// <style> tag
21        Style,
22        /// <script> tag
23        Script,
24        #[default]
25        /// any other tag
26        Other,
27    }
28
29    impl<T: AsRef<str>> From<T> for VTagKind {
30        fn from(value: T) -> Self {
31            let value = value.as_ref();
32            if value.eq_ignore_ascii_case("style") {
33                Self::Style
34            } else if value.eq_ignore_ascii_case("script") {
35                Self::Script
36            } else {
37                Self::Other
38            }
39        }
40    }
41}
42
43/// A Yew Server-side Renderer that renders on the current thread.
44///
45/// # Note
46///
47/// This renderer does not spawn its own runtime and can only be used when:
48///
49/// - `wasm-bindgen-futures` is selected as the backend of Yew runtime.
50/// - running within a [`Runtime`](crate::platform::Runtime).
51/// - running within a tokio [`LocalSet`](struct@tokio::task::LocalSet).
52#[cfg(feature = "ssr")]
53#[derive(Debug)]
54pub struct LocalServerRenderer<COMP>
55where
56    COMP: BaseComponent,
57{
58    props: COMP::Properties,
59    hydratable: bool,
60}
61
62impl<COMP> Default for LocalServerRenderer<COMP>
63where
64    COMP: BaseComponent,
65    COMP::Properties: Default,
66{
67    fn default() -> Self {
68        Self::with_props(COMP::Properties::default())
69    }
70}
71
72impl<COMP> LocalServerRenderer<COMP>
73where
74    COMP: BaseComponent,
75    COMP::Properties: Default,
76{
77    /// Creates a [LocalServerRenderer] with default properties.
78    pub fn new() -> Self {
79        Self::default()
80    }
81}
82
83impl<COMP> LocalServerRenderer<COMP>
84where
85    COMP: BaseComponent,
86{
87    /// Creates a [LocalServerRenderer] with custom properties.
88    pub fn with_props(props: COMP::Properties) -> Self {
89        Self {
90            props,
91            hydratable: true,
92        }
93    }
94
95    /// Sets whether an the rendered result is hydratable.
96    ///
97    /// Defaults to `true`.
98    ///
99    /// When this is sets to `true`, the rendered artifact will include additional information
100    /// to assist with the hydration process.
101    pub fn hydratable(mut self, val: bool) -> Self {
102        self.hydratable = val;
103
104        self
105    }
106
107    /// Renders Yew Application.
108    pub async fn render(self) -> String {
109        let s = self.render_stream();
110        futures::pin_mut!(s);
111
112        s.collect().await
113    }
114
115    /// Renders Yew Application to a String.
116    pub async fn render_to_string(self, w: &mut String) {
117        let s = self.render_stream();
118        futures::pin_mut!(s);
119
120        while let Some(m) = s.next().await {
121            w.push_str(&m);
122        }
123    }
124
125    fn render_stream_inner(self) -> impl Stream<Item = String> {
126        let scope = Scope::<COMP>::new(None);
127
128        let outer_span = tracing::Span::current();
129        BufStream::new(move |mut w| async move {
130            let render_span = tracing::debug_span!("render_stream_item");
131            render_span.follows_from(outer_span);
132            scope
133                .render_into_stream(
134                    &mut w,
135                    self.props.into(),
136                    self.hydratable,
137                    Default::default(),
138                )
139                .instrument(render_span)
140                .await;
141        })
142    }
143
144    // The duplicate implementation below is to selectively suppress clippy lints.
145    // These implementations should be merged once https://github.com/tokio-rs/tracing/issues/2503 is resolved.
146
147    /// Renders Yew Application into a string Stream
148    #[allow(clippy::let_with_type_underscore)]
149    #[tracing::instrument(
150        level = tracing::Level::DEBUG,
151        name = "render_stream",
152        skip(self),
153        fields(hydratable = self.hydratable),
154    )]
155    #[inline(always)]
156    pub fn render_stream(self) -> impl Stream<Item = String> {
157        self.render_stream_inner()
158    }
159}
160
161/// A Yew Server-side Renderer.
162///
163/// This renderer spawns the rendering task to a Yew [`Runtime`]. and receives result when
164/// the rendering process has finished.
165///
166/// See [`yew::platform`] for more information.
167#[cfg(feature = "ssr")]
168pub struct ServerRenderer<COMP>
169where
170    COMP: BaseComponent,
171{
172    create_props: Box<dyn Send + FnOnce() -> COMP::Properties>,
173    hydratable: bool,
174    rt: Option<Runtime>,
175}
176
177impl<COMP> fmt::Debug for ServerRenderer<COMP>
178where
179    COMP: BaseComponent,
180{
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        f.write_str("ServerRenderer<_>")
183    }
184}
185
186impl<COMP> Default for ServerRenderer<COMP>
187where
188    COMP: BaseComponent,
189    COMP::Properties: Default,
190{
191    fn default() -> Self {
192        Self::with_props(Default::default)
193    }
194}
195
196impl<COMP> ServerRenderer<COMP>
197where
198    COMP: BaseComponent,
199    COMP::Properties: Default,
200{
201    /// Creates a [ServerRenderer] with default properties.
202    pub fn new() -> Self {
203        Self::default()
204    }
205}
206
207impl<COMP> ServerRenderer<COMP>
208where
209    COMP: BaseComponent,
210{
211    /// Creates a [ServerRenderer] with custom properties.
212    ///
213    /// # Note
214    ///
215    /// The properties does not have to implement `Send`.
216    /// However, the function to create properties needs to be `Send`.
217    pub fn with_props<F>(create_props: F) -> Self
218    where
219        F: 'static + Send + FnOnce() -> COMP::Properties,
220    {
221        Self {
222            create_props: Box::new(create_props),
223            hydratable: true,
224            rt: None,
225        }
226    }
227
228    /// Sets the runtime the ServerRenderer will run the rendering task with.
229    pub fn with_runtime(mut self, rt: Runtime) -> Self {
230        self.rt = Some(rt);
231
232        self
233    }
234
235    /// Sets whether an the rendered result is hydratable.
236    ///
237    /// Defaults to `true`.
238    ///
239    /// When this is sets to `true`, the rendered artifact will include additional information
240    /// to assist with the hydration process.
241    pub fn hydratable(mut self, val: bool) -> Self {
242        self.hydratable = val;
243
244        self
245    }
246
247    /// Renders Yew Application.
248    pub async fn render(self) -> String {
249        let Self {
250            create_props,
251            hydratable,
252            rt,
253        } = self;
254
255        let (tx, rx) = futures::channel::oneshot::channel();
256        let create_task = move || async move {
257            let props = create_props();
258            let s = LocalServerRenderer::<COMP>::with_props(props)
259                .hydratable(hydratable)
260                .render()
261                .await;
262
263            let _ = tx.send(s);
264        };
265
266        Self::spawn_rendering_task(rt, create_task);
267
268        rx.await.expect("failed to render application")
269    }
270
271    /// Renders Yew Application to a String.
272    pub async fn render_to_string(self, w: &mut String) {
273        let mut s = self.render_stream();
274
275        while let Some(m) = s.next().await {
276            w.push_str(&m);
277        }
278    }
279
280    #[inline]
281    fn spawn_rendering_task<F, Fut>(rt: Option<Runtime>, create_task: F)
282    where
283        F: 'static + Send + FnOnce() -> Fut,
284        Fut: Future<Output = ()> + 'static,
285    {
286        match rt {
287            // If a runtime is specified, spawn to the specified runtime.
288            Some(m) => m.spawn_pinned(create_task),
289            None => match LocalHandle::try_current() {
290                // If within a Yew Runtime, spawn to the current runtime.
291                Some(m) => m.spawn_local(create_task()),
292                // Outside of Yew Runtime, spawn to the default runtime.
293                None => Runtime::default().spawn_pinned(create_task),
294            },
295        }
296    }
297
298    /// Renders Yew Application into a string Stream.
299    pub fn render_stream(self) -> impl Send + Stream<Item = String> {
300        let Self {
301            create_props,
302            hydratable,
303            rt,
304        } = self;
305
306        let (tx, rx) = futures::channel::mpsc::unbounded();
307        let create_task = move || async move {
308            let props = create_props();
309            let s = LocalServerRenderer::<COMP>::with_props(props)
310                .hydratable(hydratable)
311                .render_stream();
312            pin_mut!(s);
313
314            while let Some(m) = s.next().await {
315                let _ = tx.unbounded_send(m);
316            }
317        };
318
319        Self::spawn_rendering_task(rt, create_task);
320
321        rx
322    }
323}