remix icon indicating copy to clipboard operation
remix copied to clipboard

Children Outlet route intercepts Layout action

Open D-Rekk opened this issue 1 year ago • 4 comments

Reproduction

https://stackblitz.com/edit/remix-run-remix-2up1w8

System Info

System:
    OS: Linux 5.0 undefined
    CPU: (6) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 18.18.0 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.3 - /usr/local/bin/npm
    pnpm: 8.14.0 - /usr/local/bin/pnpm
  npmPackages:
    @remix-run/css-bundle: * => 2.7.2 
    @remix-run/dev: * => 2.7.2 
    @remix-run/node: * => 2.7.2 
    @remix-run/react: * => 2.7.2 
    @remix-run/serve: * => 2.7.2

Used Package Manager

npm

Expected Behavior

Button with submit() placed in the Layout route should trigger the action of its corresponding action, independently if the current route is nested or has query params.

Actual Behavior

Once you have submitted an action from an Outlet route and that action appends the search param ?index, when the button in the parent Layout route submits an action, that action will be intercepted by the Outlet route action (app._index.tsx) instead of it being read by the Layout route (app.tsx)

D-Rekk avatar Feb 28 '24 13:02 D-Rekk

It's not a bug. When you submit, by default it will POST to the the URL the user is currently on. Once you submitted from app._index, the URL changed to /app?index.

This meant when you submitted from your parent layout, it will POST to the app._index action. To ensure you always submit to the current layout action, specify the action prop in options. action: '.' means to POST to current route.

NOTE: useSubmit will cause a navigation to the action URL, just like <Form> submits. If you want to post without navigation then either useFetcher or add navigate: false to options.

https://remix.run/docs/en/main/hooks/use-submit#options

<button
        onClick={() => {
          submit(
            { _action: 'app.tsx' },
            {
              action: '.',
              method: 'POST',
              replace: true,
              encType: 'multipart/form-data',
              navigate: false,
            }
          );
        }}
      >

kiliman avatar Feb 28 '24 14:02 kiliman

I see, setting action: "." is what I expected to be the default behavior. It would be useful if the docs were a bit more explicative about this. For action options it says: action: The href to submit to. Default is the current route path . But it's the current visited route, not the current file route.

D-Rekk avatar Feb 28 '24 14:02 D-Rekk

Remix is trying to emulate the default browser behavior. For forms, a missing or empty action attribute will always submit to the current URL in the address bar.

Also, the browser will not submit to a partial URL without a specific action specified. When specifying paths in Remix (React-Router) in <Form action> and <Link to>, it defaults to using route-relative paths. If you want URL-relative paths, then you'll need to set relative='path'

kiliman avatar Feb 28 '24 15:02 kiliman

I think this is actually a bug in the useSubmit/fetcher.submit flows.

Remix should by default post to the contextual action - which is usually the action in the same file as the form (or the action in the nearest contextual ancestor route):

export function action () {}

export default function Component() {
  // When no action is specified - this should default to the action for this route
  return <Form method="post">...</Form>
}

This is currently working correctly for <Form>, <Form navigate={false}>, and <fetcher.Form> because they all render a default <form action> attribute value when no action prop is passed and that handles removing the ?index param so we post to the contextual route action.

That doesn't happen for useSubmit/fetcher.submit - but I think it probably should since useSubmit should be essentially interchangeable with <Form>.

export function action () {}

export default function Component() {
  let submit = useSubmit()
  // When no action is specified - this should *also* default to the action for this route
  return <button onClick={() => submit({}, { method: 'post' })}>...</button>
}

brophdawg11 avatar Feb 28 '24 17:02 brophdawg11