remix
remix copied to clipboard
Children Outlet route intercepts Layout action
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)
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,
}
);
}}
>
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.
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'
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>
}