#Server-side rendering (SSR) with useFetcher

1 messages · Page 1 of 1 (latest)

tribal oasis
#

I'm working on improving the "progressive enhancement" behaviour for my project. Since I'm using fetcher.Form, my client can submit things even without JavaScript, since it falls back to a plain HTML form until "hydration".

However, I have not been able to figure out how to populate the form with error messages in this scenario (JavaScript disabled). The component retrieves error messages from fetcher.data. At the moment, console.log(fetcher.data) shows it's always undefined on the server render, so it doesn't work and the server responds with a completely blank form.

Is this something that is possible? If so, how? It should be possible technically, since the page makes a full round trip to the server, and "respond with the same page but with errors added to the form" is a classic pure-HTML no-JavaScript thing to do, so it seems like something that would be possible in a framework like Remix, which aims to support these "web fundamentals".

And Remix does do loaders on the server as part of SSR (right?), so why isn't this working for me with actions and fetchers?

tribal oasis
#

At the moment the best I've got is to provide the errors in a loader, which would be invalidated by the action and evaluated with the SSR. This sucks because (a) for me the fetcher is tied to a component, not a route, and (b) now I probably have to save the errors to some kind of storage to communicate between actions and loaders.

verbal harness
#

I haven't tried but I think you may be able to access it with useActionData when doing SSR

tacit cave
#

I think useActionData works, conform works this way

tribal oasis
#

@verbal harness @tacit cave useActionData seems to work, sort of, but there seem to be multiple problems:

(1) useActionData only returns the most recent action for the current route. This might be a problem for hydrated pages since I'm not sure you'd recover fetcher's normal behaviour (e.g., what if you have multiple fetchers?). However, it doesn't even get to that, because useActionData flat out doesn't work on the client for actions submitted by fetchers from JS-enabled pages. It does work on the server for actions submitted by JS-disabled pages (although requests from separate fetcher.Form's cancel each other, but that's to be expected for a non-hydrated page with a full document request).

#

(2) I'm not really sure how this works with nested routing. If I have two separate fetcher.Forms in /layout and /layout/child, then how do I get two separate action responses? At the moment, my testing indicates that (for JS-disabled pages) useActionData returns the same data in the parent and the child, even if the action was only submitted to /layout/child. I'm also not sure where I can put the action for the form in in /layout, since fetcher.Form without JS degrades to a full document request, which causes a redirect to / when a form in /layout is submitted, so the only way to avoid this is to submit to /layout/child from /layout so that the redirect goes back to the same page, which seems really bad.

#

(3) This is more of a DX thing, but the "current route" restriction of useActionData is somewhat annoying, since now I have to lift up action logic out of each of my components and into actions for each route that uses them, or in the case of nested routing maybe even down into the contents of an <Outlet/>. Although, I understand that there are proposals like https://github.com/remix-run/remix/discussions/5383 that might address this.

GitHub

tl;dr allow loaders/actions anywhere in the module graph get great types and dev ergonomics would have to cross a line with our bundler we haven't crossed yet Background Remix is kind of buttho...

#

(1) seems like an actual bug, so I can try to provide an example, but basically I'm just looking in the browser console at what useActionData returns for a request submitted by <fetcher.form method="post"> and an action that just returns some fixed message, all within one route.

#

Also note @tacit cave that conform's example in their repo README uses <Form/>, not <fetcher.Form/>, and I prefer the latter since it avoids full page navigations for hydrated pages and allows parallel requests and cooperates more nicely with nested routing (although that may be my lack of understanding).

floral drum
#

The component retrieves error messages from fetcher.data

The only way fetcher.data can be populated is through a client-side fetch so anything based solely on fetcher.data will never fully work without JS. For true non-JS scenarios you will need to bring useActionData into the mix since that's what will be populated from a JS-disabled document POST request

#

(1) is accurate and due to the stateless nature of HTTP. useActionData cannot "remember" prior actions across multiple document requests. If fetcher.data is populated, use that (implied JS is enabled), if not fall back on useActionData (implies JS is disabled)

#

I'm not sure I totally follow (2) and (3) - any chance you could provide a small reproduction demonstrating the issue?

tribal oasis
#

I'm not sure I understand your point about HTTP being stateless. Are you saying this is an actual constraint, or something Remix should emulate. Why should useActionData work when Form submits a POST request, but not when fetcher.Form does the same.

#

It's late in my timezone, but I can try to give more detail about (2) and (3) tomorrow.

rotund echo
#

i often use the fetcher.data for the action submissions, and useLoaderData for loading data most of the time, rarely use the useActionData

tribal oasis
#

I would prefer fetcher.data if it worked for this purpose, but it doesn't

tribal oasis
# floral drum I'm not sure I totally follow (2) and (3) - any chance you could provide a small...

Basically (2) is saying, if you have a form in a layout that nests some page (i.e., next to the Outlet, but not in it), then it seems like the form has to submit to the "child" page route to avoid a redirect away from that page for a JS-disabled form submission, which is bad. This is because currently (I think) all JS-disabled POST requests are also document requests for the document at the same route as the action.

#

It's "bad" because now your code is coupled (in my opinion) in a really unintuitive way and if there were multiple pages nested by the same layout, then it wouldn't even work for more than one of them.

tribal oasis
floral drum
# tribal oasis I'm not sure I understand your point about HTTP being stateless. Are you saying ...

The stateless comment is in response to "useActionData only returns the most recent action for the current route". I may have misinterpreted, but that sounded like you wanted useActionData to remember actions beyond the most recent one.

Why should useActionData work when Form submits a POST request, but not when fetcher.Form does the same

useActionData represents the navigational action data. This means a <Form> or <fetcher.Form> when JS is disabled. When JS is enabled, the fetcher is non-navigational and the data goes into fetcher.data.

verbal harness
# tribal oasis I'm not sure I understand your point about HTTP being stateless. Are you saying ...

useActionData returns the <Form> data, the same way fetcher.data returns the <fetcher.Form>, the things is that without JS enabled, <fetcher.Form> and <Form> works as <form>, and when doing a POST without JS you get the action data with useActionData

and because how the browser works, if you submit a <form> (no JS) there's only one POST request at the same time, if you submit another form that's a different POST and you have no way to know the previous POST, unless you add it as a hidden input to every form in your web

floral drum
verbal harness
floral drum
tribal oasis
verbal harness
verbal harness
tribal oasis
#

Are you saying there's no way currently to link to a fetcher instance, or it couldn't be done, or it shouldn't be done?

verbal harness
#

there's no way, and I doubt it can be done

#

the useFetcher hook doesn't know what URL you're going to fetch

#

it's only when a fetcher.Form is submitted, or you call fetcher.load or fetcher.submit that the URL is known by the fetcher, but without JS this never happens because your fetcher.Form becomes a <form> HTML tag

#

also what happens if a fetcher is used in a list of items (e.g. to delete the item), each fetcher will POST to the same URL and the same action, with different form data

#

if Remix linked it to one specific URL/action it would have to consider each item in the list as the same fetcher and populate their fetcher.data

floral drum
verbal harness
#

also I think if you really want to support no-JS scenarios your actions should always redirect

#

in that case, you don't have an issue with how to access the action data

#

because there's no action data to access, you redirect and if you want to send a message to the UI from the action you use session.flash to set it and session.get in a loader to read it

#

and this will work without JS, with JS using <Form> and with JS using <fetcher.Form>

tribal oasis
#

What if your action is redirecting to the same page, that seems wasteful

tribal oasis
#

Then there would be enough information to populate it on the server as part of a document request

#

The url would have to be appended on the server ofc before the original document with the form was provided

verbal harness
#

by using a redirect you avoid that, even if you redirect to the same page

tribal oasis
#

Ah yes, post-redirect-get, forgot about that

tribal oasis
#

I mean I could just make my own wrapper component that does this

#

It would just be nice if fetcher.Form could do it on the server for document request submissions without extra work that's all

verbal harness
tribal oasis
#

That's what I meant

#

I just think it's weird to, e.g., create an action on every page for a form in the reusable footer layout

verbal harness
#

you need a way to always get the same fetcher "id" for each fetcher, even across document requests, but using it in an list makes it harder

tribal oasis
#

I agree that lists are problematic

#

Even for react

#

Which is why they force key on you

verbal harness
# tribal oasis That's what I meant

you don't need to do that, if your footer is on a layout route it can have a fetcher.Form inside that will automatically point to that layout route action

another option is to move the action to a resource route and point the <fetcher.Form action> there, then redirect from the action and it will work, and if you want to avoid the redirect if JS is enabled you can send if the user has JS in a hidden input https://sergiodxa.com/articles/progressively-enhance-the-usefetcher-hook-in-remix

verbal harness
# tribal oasis Which is why they force key on you

exactly, so Remix would need to do the same for fetcher, ask for an unique ID just to support this, with the difference that React keys are unique per list but fetcher ids would need to be globally unique

tribal oasis
#

I probably had something backwards in how my layout route was set up

floral drum
tribal oasis
floral drum
#

You can provide your own key to fetchers these days (which is new as of 2.2.0) so there is some potential for you to do your own "remembering of fetchers" - you would just need to wire up some of your own logic through the standard action/useActionData

tribal oasis
#

Is the fetcher key exposed by some property or method?

#

Or no, you can only know it if you provide it

verbal harness
#

since you set the fetcher key, you can include it as a hidden input I imagine

tribal oasis
#

That's what I'm thinking I'll have to do. I already have a custom form component that does a lot on top of fetcher.Form, so I'll just have to extend it some more, but that's for tomorrow

#

That's something middleware would be nice to have for

#

Would save boilerplate in every action (and I already have a lot for CSRF, sessions, etc.)

floral drum
#

Fetcher keys are exposed on useFetchers but I would only rely on them if you are providing an explicit key via useFetcher( key: 'whatever' }) and you want to look it up elsehwere. Otherwise, the fetcher key is a automatically unique/internal incrementing identifier like __1__, __2__ and the values will not be stable across renders as they are just icnrementing IDs based on the order in which fetchers mount during client side hydration/render

tribal oasis
#

If they're not stable across renders then doesn't that cause the issue @verbal harness said they were needed to avoid

verbal harness
#

I imagine there's a default one

#

but you should provide one if you want to use it

#

and you can use something like a list item ID as key

#
let fetcher = useFetcher({ key: `todo:${id}:delete` })
floral drum
#

Right - they are stable if you provide your own known key

#

useFetcher() keys are unstable, useFetcher{ key: 'known-string' }) are stable

tribal oasis
#

What's the point of having a default if the default is unstable and therefore could break on rerender

floral drum
#

We're talking non-JS world here - they are not stable across document requests

#

They are stable across JS-rerenders in a hydrated app

verbal harness
#

remember fetcher is intended for JS-is-enabled world

tribal oasis
#

Challenge accepted

verbal harness
#

e.g. fetcher.submit will never work without JS, fetcher.Form does because it renders a <form>

tribal oasis
#

that's why I almost always prefer fetcher.form

#

It's my favourite form component 🥰. Just haven't gotten it to work re progressive enhancement yet