QwikDev / qwik-evolution

Home for Qwik proposals and RFCs
15 stars 0 forks source link

RFC: `usePreventNavigate` #15

Open wmertens opened 2 months ago

wmertens commented 2 months ago

Proposed in #14

Champion

@wmertens

Summary:

usePreventNavigate((url?: URL) => boolean | Promise<boolean>)

What's the motivation for this proposal?

Problems you are trying to solve:

Goals you are trying to achieve:

Proposed Solution / Feature

What do you propose?

We add usePreventNavigate(cb), which registers a handler that will be called when there is a navigation event. When the handler returns true, navigation is blocked.

This is actually not trivial to implement correctly because not all navigation passes through qwik-city, and when the browser is involved the answer need to be synchronous.

If the navigation attempt is going through qwik-city, the callback will receive the URL of the desired target. This makes it possible to perform the navigation later.

Code examples

using a modal library:

export default component$(() => {
  const okToNavigate = useSignal(true);
  usePreventNavigate$((url) => {
    if (!okToNavigate.value) {
      if (!url) return true;
      return confirmDialog(
        `Do you want to lose changes and go to ${navSig.value}?`
      ).then(answer => !answer);
    }
  });

  return (
    <div>
      <button onClick$={() => (okToNavigate.value = !okToNavigate.value)}>
        stuff
      </button>
      <br />
      more stuff
    </div>
  );
});

Using a separate modal:

export default component$(() => {
  const okToNavigate = useSignal(true);
  const navSig = useSignal<URL | number>();
  const showConfirm = useSignal(false);
  const nav = useNavigate();
  usePreventNavigate$((url) => {
    if (!okToNavigate.value) {
      if (url) {
        navSig.value = url;
        showConfirm.value = true;
      }
      return true;
    }
  });

  return (
    <div>
      <button onClick$={() => (okToNavigate.value = !okToNavigate.value)}>
        stuff
      </button>
      <br />
      more stuff
      <br />
      {showConfirm.value && (
        <div>
          <div>
            Do you want to lose changes and go to {String(navSig.value)}?
          </div>
          <button
            onClick$={() => {
              showConfirm.value = false;
              okToNavigate.value = true;
              nav(navSig.value!);
            }}
          >
            Yes
          </button>
          <button onClick$={() => (showConfirm.value = false)}>No</button>
        </div>
      )}
    </div>
  );
});

PRs/ Links / References

PRs:

https://github.com/QwikDev/qwik/pull/6825

Reference:

Remix Router has useBlocker, there are interesting tidbits in their decisions document.

thejackshelton commented 2 months ago

So that I understand correctly, if the user was to navigate to this form page, and then fill out a form, let's say they hit the browser back button, this would prevent them from going to the previous page?

I sometimes do this on accident and see that the browser sometimes prevents me from going back to the previous URL and I'm relieved. I wonder at what point this would be a frustrating experience.

wmertens commented 2 months ago

@thejackshelton yes correct. I hope it's not a frustrating experience when the navigation is only prevented when there are changes. By making it a hook, it can get at all the metadata it needs to make that decision.

AnthonyPAlicea commented 2 months ago

This is terrific functionality, and a common need in web apps when the page is in a dirty state. The UX way to avoid frustration is to ensure there is either:

  1. An explicit cancelation workflow when usePreventNavigate is used (like a Cancel button next to a Save button), or
  2. An auto-save feature, where preventing navigation happens when data has been changed and autosave hasn't yet triggered, but once auto-save triggers then navigation can occur.

These wouldn't need to be enforced in any way, but could be mentioned in documentation as best practices.

shairez commented 1 month ago

thanks @AnthonyPAlicea ! Important point!

Now that it's merged and will soon be released as "experimental" we would love a docs PR with your expertise sprinkled all over it 🙏🙌