#SSR Logistics

1 messages Β· Page 1 of 1 (latest)

broken widget
#

Hi! I'm new to yew and to frontend frameworks in general. Previously I have only used templating engines like Tera/Jinja.

I'm just testing yew atm before I start with my project to get some understanding, and I'm struggling to understand how SSR works.

I would like to emulate something that I'm used to with Tera, where the server can provide the page with extra data and then render on demand. I assume this would be equivalent to "hydration" in yew lingo.

From the limited examples and github issues, I have gathered that hydrate content needs to be slotted inside the body element, otherwise it won't work, crashing yew with expected Component(App) start, found node type 1.

From the docs I understood that the point of suspend/hydration is to allow the client to receive part of the page, eg display a loading screen, and then receive the remainder of the data afterwards, however I don't see how this makes sense if the hydration body must be slotted in the middle of the loading page. I would expect that you could stream the content to the client, where the first data sent would be the loading page, and then after a few seconds of server-side code, the remainder of the page would be sent afterwards.

I don't think I'm understanding this correctly, so I'm looking to have my understanding corrected.

dry bough
#

From the limited examples and github issues, I have gathered that hydrate content needs to be slotted inside the body element, otherwise it won't work, crashing yew with expected Component(App) start, found node type 1.
This is how you usually do it, but it's not limited to <body/>. it can be in other nodes too. you can use Renderer::with_root() for this

#

From the docs I understood that the point of suspend/hydration is to allow the client to receive part of the page, eg display a loading screen, and then receive the remainder of the data afterwards, however I don't see how this makes sense if the hydration body must be slotted in the middle of the loading page. I would expect that you could stream the content to the client, where the first data sent would be the loading page, and then after a few seconds of server-side code, the remainder of the page would be sent afterwards.
You can send the data directly with the SSR response, using use_prepared_state() (or if it's just static html, inline it).

broken widget
#

I tried looking at use_prepared_state and i got confused πŸ˜…

dry bough
#

yeah the signature is kind of confusing in the docs, but it's not too bad to use it

#

in the end basically the same as use_future_with_deps()

broken widget
#

I've not used that either

dry bough
#

But you can also just go the route of loading the data later. but that way I don't think you really need hydration/ssr

#

just suspension

broken widget
#

Okay so i have the example counter app, but i'd like a h1 tag to be populated with a string from the server

#
use yew::prelude::*;

pub enum Msg {
    AddOne,
}

pub struct App {
    value: i64,
}

#[derive(Properties, Clone, Default, PartialEq)]
pub struct AppProps {
    pub string: Option<String>
}

impl Component for App {
    type Message = Msg;
    type Properties = AppProps;

    fn create(ctx: &Context<Self>) -> Self {
        Self { value: 0 }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::AddOne => {
                self.value += 1;
                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        let fallback = html!{ <h1>{"Loading..."}</h1> };

        html! {
            <Suspense {fallback}>
                <div>
                    <h1>{ ctx.props().string.as_ref().map(String::as_str).unwrap_or_default() }</h1>

                    <button onclick={ctx.link().callback(|_| Msg::AddOne)}>{ "+1" }</button>
                    <p>{ self.value }</p>
                </div>
            </Suspense>
        }
    }
}
#

This is what I tried

#

I realise i probably don't need to map to a str if i'm using as_ref but that's unimportant πŸ˜„

dry bough
broken widget
#

😭

dry bough
#

You can just wrap the component in a functional component tho

broken widget
#

I dont know the difference, I've only just started using it today and copied the example

dry bough
#

besides suspension they can both do everything. it's really the only difference afaik

#

although, maybe prepared state is also a function component only thing, not sure actually

broken widget
#

I was hoping yew has something built in for that, instead of requiring repeated requests from the server

dry bough
#

What do you mean? To keep in sync with the server?

#

Yew doesn't really have something like that. SSR is just a one time thing for when requesting the first page load, after that it's basic old yew

#

but yeah if the data doesn't change after loading, then SSR can work

broken widget
#

Okay so, how would i do a user page for example? The server would need to provide the display name for the current logged in user (probably based on a session token), and let's say this just gets rendered to a h1 tag

dry bough
#

That would work with SSR, just generate the html on the server and send it over. You wouldn't need Suspension, as it's directly loaded and filled in on the server.

#

but you need to have the same data on the client to hydrate though, that's where prepared state comes in

broken widget
#

and this would be where i slot in the SSR result into the body tag before responding tot the request

dry bough
broken widget
dry bough
#

the fetch_uuid() would happen on the server, and would send the html (filled in) + the data through prepared state

dry bough
broken widget
#

Does the client need None for hydrate?

dry bough
#

No,

#[function_component]
fn Content() -> HtmlResult {
    let uuid = use_prepared_state!(async move |_| -> Uuid { fetch_uuid().await }, ())?.unwrap();

    Ok(html! {
        <div>{"Random UUID: "}{uuid}</div>
    })
}

#[function_component]
pub fn App() -> Html {
    let fallback = html! {<div>{"Loading..."}</div>};

    html! {
        <Suspense {fallback}>
            <Content />
        </Suspense>
    }
}

here the {uuid} would have both a value on the server and on the client.

broken widget
#

I don't understand what the point of fallback is, if the ssr result needs to be added in the body tag

dry bough
#

but the for the client the data would just be serialized and send over during page load, while on the server it would execute fetch_uuid()

#

Well actually in this example I don't think it would ever render the fallback πŸ€”

broken widget
#

how would you render the fallback?

#

that's my confusion 😭

dry bough
#

Ah wait i get the example, that's because if the app is run without SSR, it would do the fetch on the client, and then it would basically just be a use_future() and the fallback would be used for when the futurs hasn't returned yet

#

The fallback is for when fetching data on the client side

#

anyway yeah, its kind of a tricky thing and hard to understand sometimes

broken widget
#

hmm

dry bough
#

I think your conception of SSR/hydration is a bit wrong though. SSR/Hydration is really only to speed up first load pages up, so you don't have to fetch the data from the server after loading yew on the client, by sending the data directly with the first page load. After that if you want to change the data, you would have to do normal fetching again. So it's not like a magically sync between server and client.

#

And then suspension becomes useful with the fallback

broken widget
#
// Client: 
#[derive(Properties, PartialEq, Default)]
pub struct AppProps {
    pub title: Option<AttrValue>
}

#[function_component]
pub fn App(props: &AppProps) -> HtmlResult {
    Ok(html! {
        <h1>{ props.title.clone() }</h1>
    })
}

fn main() {
    yew::Renderer::<App>::new()
        .hydrate();
}

// Server:
async fn index() -> String {
    use client::{ App, AppProps };

    let shell = tokio::fs::read_to_string("public/index.html").await.expect("oof");

    let data = yew::ServerRenderer::<App>::with_props(move || AppProps { title: Some("awawa".into()) }).render().await;

    shell.replace("<body>", &format!("<body>{data}"))
}
#
panicked at 'called `Result::unwrap()` on an `Err` value: JsValue(NotFoundError: Node.removeChild: The node to be removed is not a child of this node
dry bough
#

Hmm that's odd, i don't see anything wrong

#

Ah, probably because rust yew::Renderer::<App>::new() .hydrate(); doesn't have the state for AppProps

broken widget
#

it uses Default::default i'm pretty sure

#

hm it didn't like the None

#

using <h1>{ props.title.as_ref().unwrap_or(&"loading...".into()) }</h1> instead makes it flash the correct message and then return to loading... for some reason

dry bough
#
// Client: 
#[function_component]
pub fn App() -> HtmlResult {
    let title = use_prepared_state(async |_| -> String { "awawa".to_string() }, ())?.unwrap();
    Ok(html! {
        <Header title={title}/>
    })
}

#[derive(Properties, PartialEq, Default)]
pub struct HeaderProps {
    #[prop_or_default]
    pub title: Option<AttrValue>
}


#[function_component]
pub fn Header(HeaderProps{ title }: &HeaderProps) -> Html {
  html!{
    <h1>{ title }</h1>
  }
}
fn main() {
    yew::Renderer::<App>::new()
        .hydrate();
}

// Server:
async fn index() -> String {
    use client::{ App, AppProps };

    let shell = tokio::fs::read_to_string("public/index.html").await.expect("oof");

    let data = yew::ServerRenderer::<App>::new().render().await;

    shell.replace("<body>", &format!("<body>{data}"))
}

try this? Might contain some errors

broken widget
#

i want the string to exist on the server

#

it's hard coded for testing but ideally it'll come from the db

dry bough
#

it does you can get it in the use_prepared_state, that does exist on the server

#

now it just returns a hard coded value, but you can get it from the db of course

broken widget
#

but the closure is on the client

dry bough
#

no it applies some magic and converts it basically just to an Some(data) on the client

broken widget
#

what is deps argument from use_prepared_state

dry bough
#

That's for when its depend on some input for that component, like a user id e.g.

broken widget
#

yes, how do i do that

dry bough
#

so as long as the deps are the same as what was calculated on the server, it returns what the server had. else it returns None

broken widget
#

where do i provide the deps on the server side?

dry bough
#
// Client: 
#[function_component]
pub fn App() -> Html {
    let user_id = use_state(|| 1);
  
    let onclick = {
      let user_id = user_id.clone();
      Callback::from(move |_| {
        user_id.set(*user_id + 1);
      })
    };  

    html! {
        <div>
          <button {onclick}>{"Clicky"}</button>
          <Suspense>
            <User id={*user_id}/>
          </Suspense>
        </div>
    }
}

#[derive(Properties, PartialEq, Default)]
pub struct UserProps {
    #[prop_or_default]
    pub id: usize,
}


#[function_component]
pub fn User(UserProps{ id }: &UserProps) -> HtmlResult {
    let name = use_prepared_state(async |id| -> String { get_user_name(id).await }, *id)?;
    // if name is None, gotta get it from the server somehow, e.g. use_future(). Which will happen when you press the button

    Ok(html! {
      <div>{ format!("user name: {name}") }</div>
    })
}

fn main() {
    yew::Renderer::<App>::new()
        .hydrate();
}

// Server:
async fn index() -> String {
    use client::{ App, AppProps };

    let shell = tokio::fs::read_to_string("public/index.html").await.expect("oof");

    let data = yew::ServerRenderer::<App>::new().render().await;

    shell.replace("<body>", &format!("<body>{data}"))
}
#

something like that

#

ofcourse you probably want to get the user ID from a page query like www.app.rs/user/1/ and pass that to the component

broken widget
#

is there not any way to pass it in the same request

#

since the server needs a special route to hydrate

dry bough
#

yes of course, with yew_router and page queries

broken widget
#

I don't mean that, i'm using my own choice of backend

#

I already have an index route because i need to hydrate, aka read the file and append the result from the server renderer

#

i want to provide data within that server renderer

#

much like how i'd do it with a templating engine

dry bough
#

Well you can pass it as properties to App like you had before

broken widget
#

yes but it wasn't working

dry bough
#

that's because you didn't pass it into use_prepared_state

#

if you do, as a dep, it should work

broken widget
#

hmmm

#

so what's the point of having the use_prepared_state function at all, if i'm just using the value from the props

dry bough
#

It is, you just need to pass which prop you want to use

#

ah wait i misread that

dry bough
broken widget
dry bough
#

Simple add use_prepared_state

// Client: 
#[derive(Properties, PartialEq, Default)]
pub struct AppProps {
    pub title: Option<AttrValue>
}

#[function_component]
pub fn App(props: &AppProps) -> HtmlResult {
    let title = props.title.clone();
    let title = use_prepared_state(async move |_| -> AttrValue { title.clone() }, ())?;
    Ok(html! {
        <h1>{ title.clone() }</h1>
    })
}

fn main() {
    yew::Renderer::<App>::new()
        .hydrate();
}

// Server:
async fn index() -> String {
    use client::{ App, AppProps };

    let shell = tokio::fs::read_to_string("public/index.html").await.expect("oof");

    let data = yew::ServerRenderer::<App>::with_props(move || AppProps { title: Some("awawa".into()) }).render().await;

    shell.replace("<body>", &format!("<body>{data}"))
}

but it's a bit of a contrived example obviously

#

and might not work as there is no <Suspense> which i think is required

broken widget
#

AttrValue isn't (De)Serialize 😭 😭

#

I still don't understand what use_prepared_state is supposed to do

broken widget
#

Okay I had another look at it this morning and I think I understand it better.

The use_prepared_state macro will always return None during client side rendering (aka Renderer::hydrate()), but during server side rendering (aka ServerRenderer::render()) it will call the closure inside.

My issue is I don't want the backend logic inside the frontend codebase. I would like to keep them separated into two separate crates.

dry bough
#

My issue is I don't want the backend logic inside the frontend codebase. I would like to keep them separated into two separate crates.
Personally I would keep the yew parts as mostly inline, so put it in a seperate crates that both your frontend and backend use. For the parts that are different you can provide a provider of sorts from the frontend or the backend that has the same interface. Anyway, that's what I do πŸ™‚

broken widget
#

Yeah I'm gonna try a different structure than what I'm used to

#

I'm gonna try SPA with only client side rendering, that's the easiest option i think