mefechoel / svelte-navigator

Simple, accessible routing for Svelte
Other
504 stars 39 forks source link

Example of async private route #29

Closed oldtinroof closed 3 years ago

oldtinroof commented 3 years ago

Firstly, thanks for creating this module - it's been a great help with creating my first couple of Svelte apps :)

In my latest app I've implemented the private route guard as in the example, and it works great when checking a simple bool or object on the store.

My auth requires an async call (to establish a JWT refresh on page reload), but I can't quite work out how to fit it in.

I thought I could use onMount, but that seems to be called after the reactive declaration. I also tried another 'checking' route that the private route could forward to on first load/browser refresh - which I would aim to handle the async check, but couldn't get that to work either.

Do you have an example how I might do this? Or if you can point me in the right direction, that would be much appreciated.

My current code looks like this:


    import { onMount } from 'svelte';
    import { useNavigate, useLocation } from 'svelte-navigator';
    import { checkAuthStatus, isAuthenticated } from '../stores/auth';

    const navigate = useNavigate();
    const location = useLocation();

    const navigateToLogin = () => {
        navigate('/login', {
            state: { from: $location.pathname },
            replace: true,
        });
    };

    onMount(async () => {
        try {
            await checkAuthStatus();
        } catch {
            navigateToLogin();
        }
    });

    $: if (!$isAuthenticated) {
        navigateToLogin();
    }
</script>

{#if $isAuthenticated}
    <slot />
{/if}
mefechoel commented 3 years ago

Hey there! Yes, the example code doesn't quite work here since it asumes you're either logged in or not, but it doesn't take loading into account. To kake this work you'll need to add a state that indicates the check is still pending and use it to prevent redirecting. Something like this might work:

<script>
    import { onMount } from 'svelte';
    import { useNavigate, useLocation } from 'svelte-navigator';
    import { checkAuthStatus, isAuthenticated } from '../stores/auth';

    let isLoading = true;

    const navigate = useNavigate();
    const location = useLocation();

    const navigateToLogin = () => {
        navigate('/login', {
            state: { from: $location.pathname },
            replace: true,
        });
    };

    onMount(async () => {
        try {
            await checkAuthStatus();
        } catch {
            // the reactive handler will take care of this...
            // navigateToLogin();
        } finally {
            isLoading = false;
        }
    });

    $: if (!$isAuthenticated && !isLoading) {
        navigateToLogin();
    }
</script>

{#if $isAuthenticated && !isLoading}
    <slot />
{/if}
mefechoel commented 3 years ago

Although you might have to put the navigate call back into the reactive handler. Svelte might not re run it when location changes otherwise.

oldtinroof commented 3 years ago

Ah great, thanks for getting back to me so quickly - I'd not thought about a pending check variable. That works great!

I'm now getting the a11y console warning "Could not find an element to focus on" - all of my routes have H1s - the route guard files are the only ones that don't. Do I need to turn something off on each of them?

mefechoel commented 3 years ago

Yes, that makes sense. Svelte Navigator tries to focus a heading whenever a new route is rendered. But when the guard renders there are no children yet, so it cant find a heading. What you can do is to manually pass down a registerFocus action that you create in the guard using useFocus. If you call useFocus in the guard it tells Svelte Navigator that you'll manage focus manually and waits for an element to be selected via the registerFocus action. This could look something like this:

<script>
    import { onMount } from 'svelte';
    import { useNavigate, useLocation, useFocus } from 'svelte-navigator';
    import { checkAuthStatus, isAuthenticated } from '../stores/auth';

    let isLoading = true;

    const navigate = useNavigate();
    const location = useLocation();
    const registerFocus = useFocus();

    // All the same as before...

    $: if (!$isAuthenticated && !isLoading) {
        navigateToLogin();
    }
</script>

{#if $isAuthenticated && !isLoading}
    <!-- Pass the registerFocus action to the children -->
    <slot {registerFocus} />
{/if}

And then use the registerFocus inside the slot, or pass it to a child component and use it in there:

<PrivateRoute let:registerFocus>
    <h1 use:registerFocus>Psst, private...</h1>
</PrivateRoute>

I know this is a little more complex, but it keeps the focus management working as expected, so the private routes can also benefit from the a11y features.

oldtinroof commented 3 years ago

Thanks for that - it's now working, and doing what I'd hoped - I appreciate your thorough answers!

Would it help if I submitted a PR with an additional example that handles this use case?

mefechoel commented 3 years ago

Great!

If you could submit a pr with your setup, that would be awesome! I guess most apps will use some sort of async authentication so this will be much more helpful :)

oldtinroof commented 3 years ago

No worries, I'll sort it this weekend 👍

mefechoel commented 3 years ago

@oldtinroof Thanks a lot for helping out here! I just merged the PR and moved the REPL example to my account, so I can edit it in the future.