leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
15.29k stars 600 forks source link

Server action's value is None after calling the action with JS/WASM disabled #2321

Open haslersn opened 4 months ago

haslersn commented 4 months ago

Describe the bug

The following page allows you to enter some text and submit it. The text should then appear under the form. However, with JS/WASM disabled, it does not appear, because the page gets rendered on the server where action.value().get() is None.

use leptos::*;
use leptos_meta::*;
use leptos_router::*;

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();

    let action = create_server_action::<Identity>();
    let value = action.value();
    view! {
        <ActionForm action>
            <input type="text" name="x"/>
            <button type="submit">Submit</button>
        </ActionForm>
        {move || value.get()}
    }
}

#[server(Identity)]
async fn identity(x: String) -> Result<String, ServerFnError> {
    Ok(x)
}

Leptos Dependencies

leptos = { version = "0.6", features = ["nightly"] }
leptos_axum = { version = "0.6", optional = true }
leptos_meta = { version = "0.6", features = ["nightly"] }
leptos_router = { version = "0.6", features = ["nightly"] }

Expected behavior

See bug description above.

haslersn commented 4 months ago

The same bug also happens in case the server function returns an error, even though the server could deserialize the error value from the query string that gets added during redirect.

Example:

use leptos::*;
use leptos_meta::*;
use leptos_router::*;

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();

    let action = create_server_action::<Identity>();
    let value = action.value();
    view! {
        <ActionForm action>
            <input type="text" name="x"/>
            <button type="submit">Submit</button>
        </ActionForm>
        {move || format!("{:?}", value.get())}
    }
}

#[server(Identity)]
async fn identity(x: String) -> Result<String, ServerFnError> {
    Err(ServerFnError::new(x))
}

In this example, when you submit "text", you get redirected to

/?__path=%2Fapi%2Fidentity3724951852840807129&__err=ServerError%7Ctext

but the server doesn't seem to pick up the value from the query and instead renders None.

gbj commented 4 months ago

The same bug also happens in case the server function returns an error, even though the server could deserialize the error value from the query string that gets added during redirect.

That's odd. When I run the example code you provided, here's what I get: Screenshot 2024-02-17 at 7 34 30 AM

(It might be worth checking against the main branch, which is what I'm testing against.)

gbj commented 4 months ago

The first and separate issue about the successful value not being passed through is working as designed. When implementing the (relatively new) feature of passing error results through, I tried to do the same for the success values and found it to be impossible or to require additional constraints. Given more coffee and an hour or so of free time to replicate what I tried to do, I could probably give a better answer.

I will mark the original issue as a feature request as it's currently WAD, but worth exploring more.

If you are able to create a reproduction of the second issue (with errors) could you open it as a separate issue so they can be handled individually? Thanks.

haslersn commented 4 months ago

I tried to do the same for the success values and found it to be impossible or to require additional constraints.

Maybe it could be implemented without a redirect:

gbj commented 4 months ago

Maybe it could be implemented without a redirect:

  • The form's action= needs to point to the same page the user is currently on

Yes, this part is essential. I looked into doing it in a single flight, but the issue is that (with server fns or any one REST-style API) you would land on the server fn URL, not the URL of the page — AFAIK there is no way to say "redirect back to this URL but don't load that second page from that URL, instead use this HTML."

This would require some other construct that doesn't currently exist, in which you could declare that some route X only uses a single server function Y, and so dispatching a POST to X should act as if you're calling Y.

Some exploratory work on "POST to this page" has been done in #1120.


The typical way to handle the case above, FWIW, would be to use a resource to load the new data. (Think of a TODO list or whatever: you would see the updated list of TODOs if it succeeded, and you would see the error message if it failed; in a no-JS environment, you lose a nice "Action succeeded" type of message, but that's the extent of the degradation.)

haslersn commented 4 months ago

This would require some other construct that doesn't currently exist, in which you could declare that some route X only uses a single server function Y

That's not required if you specify Y within the request by using a hidden form field.