Skip to content

Router context invalidate Promise resolve does not invalidate context #2072

Closed

Description

@ZahidHasan321

Describe the bug

In the code below, which was suppose to invalidate the context and navigate to the following route. for context i'm following the example given in tanstack authenticated routes example. Even after navigation the context isn't invalidated which I can see in console log. It results in redirect to login page not working properly after clicking logout and just stuck in the "/" page.

router.invalidate().finally(() => { navigate({ to: "/" }); });

It only invalidates or I can see it invalidates when I manually reload the page, which means during the navigation the context doesn't invalidate.

PS: I do have an api request in the logout function.

Your Example Website or App

...

Steps to Reproduce the Bug or Issue

Following the tanstack example locally.

https://stackblitz.com/edit/tanstack-router-4achrw?file=src%2Froutes%2F_auth.tsx

Expected behavior

Router context should've been already invalid in .finally or .then even before navigating.

Screenshots or Videos

No response

Platform

  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Safari, Firefox]
  • Version: [e.g. 91.1]

Additional context

No response

Activity

  1. robertyulisman commented on Aug 9, 2024

    @robertyulisman

    i got same issue, and stuck with this, any solution ? when i am use authenticated using local storage, there is no problem, but when using api, not navigate after login success

  2. ZahidHasan321 commented on Aug 14, 2024

    @ZahidHasan321
    Author

    i got same issue, and stuck with this, any solution ? when i am use authenticated using local storage, there is no problem, but when using api, not navigate after login success

    Still no reply from the devs as of now

  3. ilia-luk commented on Aug 20, 2024

    @ilia-luk

    same issue here

  4. schiller-manuel commented on Aug 20, 2024

    @schiller-manuel
    Collaborator

    can you please provide a minimal complete example by forking one of the router examples on stackblitz?

  5. ZahidHasan321 commented on Aug 20, 2024

    @ZahidHasan321
    Author

    can you please provide a minimal complete example by forking one of the router examples on stackblitz?

    https://stackblitz.com/edit/tanstack-router-4achrw?file=src%2Froutes%2F_auth.tsx

    Here a link to a fork, I've only made one change to this, instead of navigating to "/" which is a public route, it navigates to "login", which shouldn't be a problem if context is invalidated, however it isn't. so even after clicking logout, it doesn't go back to "login" route.

  6. mkhennoussi commented on Aug 22, 2024

    @mkhennoussi

    Same here while using supabase auth api. No issue when working with local storage.

    This is not working : https://tanstack.com/router/latest/docs/framework/react/guide/router-context#invalidating-the-router-context

  7. mkhennoussi commented on Aug 22, 2024

    @mkhennoussi

    update guys, the current hack seems to use sleep(1000) as explained here :
    #1668

    // This is just a hack being used to wait for the auth state to update
    // in a real app, you'd want to use a more robust solution
          await sleep(1)

    This trick works on my side, but this is a serious issue with Context.

    PS: the sleep function is quite obvious :

    export async function sleep(ms: number) {
      return new Promise((resolve) => setTimeout(resolve, ms));
    }
  8. ilia-luk commented on Aug 23, 2024

    @ilia-luk

    I'm currently using the flushSync solution as a workaround following suggestion by SzelamC at #1668 (reply in thread)

  9. ZahidHasan321 commented on Aug 23, 2024

    @ZahidHasan321
    Author

    I'm currently using the flushSync solution as a workaround following suggestion by SzelamC at #1668 (reply in thread)

    So where should I use flushSync regarding this issue?

  10. mkhennoussi commented on Aug 23, 2024

    @mkhennoussi

    Thanks @ilia-luk, it works great, I didn't thought about this solution.

    @ZahidHasan321 use flushSync in your AuthProvider when you set your new user/session state like in the thread mentionned by @ilia-luk .

  11. syama-aot commented on Sep 6, 2024

    @syama-aot

    I've noticed that the issue was not there in version 1.16.0 of @tanstack/react-router, but it was found in the latest release, version 1.56.1. Could you provide a proper solution or let us know if there is a patch release available?

  12. joemark0008 commented on Jan 11, 2025

    @joemark0008

    Got same issue here

    "@tanstack/react-router": "^1.95.3",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",

  13. Rick-Phoenix commented on Jan 14, 2025

    @Rick-Phoenix

    Same here. I am updating the context passed to the router, but while the context's value gets updated correctly, the loader functions that are triggered with router.invalidate() still load the older context value.

    Only when the route is reloaded manually the router shows the updated context.

    I've tried doing this with any given combination of useState, useEffect and contextProvider and still same result.

    As others have mentioned, a workaround for this involves using some kind of synchronous external store instead of the router context (in my case, I am using Tanstack Query, so I am grabbing the queryClient from the router context and using queryClient.getQueryData to get the updated value).

  14. CyrilStrone commented on Mar 14, 2025

    @CyrilStrone
    Contributor

    I also encountered this problem and found that context does not always update correctly when calling router.invalidate().

    Previously, I used beforeLoad like this:

    beforeLoad: (props) => {
      const isPublic = !!props.matches.find(match => match.id === '/pu');
      const isPrivate = !!props.matches.find(match => match.id === '/pr');
    
      const isAuthenticated = props.context.auth.isAuthenticated;
    
      if (isAuthenticated !== undefined) {
        if (isAuthenticated) {
          if (isPublic) {
            throw redirect({ to: '/pr' });
          }
        } else {
          if (isPrivate) {
            throw redirect({ to: '/pu' });
          }
        }
      }
    };

    However, I noticed that context does not always update properly after router.invalidate().

    As a temporary workaround, I moved the authentication check to the root component like this:

    const isAuthenticated = useMemo(() => (isFetched ? isSuccess : undefined), [isFetched, isSuccess]);
    
    const navigate = useNavigate();
    
    const isMatchPrivate = useMatches({
      select(matches) {
        return matches.some(match => match.fullPath === LayoutRoutePrivate.fullPath);
      }
    });
    const isMatchPublic = useMatches({
      select(matches) {
        return matches.some(match => match.fullPath === LayoutRoutePublic.fullPath);
      }
    });
    
    useEffect(() => {
      if (isAuthenticated !== undefined) {
        if (isMatchPrivate) {
          if (!isAuthenticated) navigate({ to: '/pu' });
        } else if (isMatchPublic) {
          if (isAuthenticated) navigate({ to: '/pr' });
        }
      }
    }, [isAuthenticated, isMatchPrivate, isMatchPublic, navigate]);

    This approach ensures that authentication status is always checked based on updated state. However, it would be great to have a proper fix for beforeLoad so that context updates correctly after router.invalidate().

  15. 13 remaining items

  16. CyrilStrone commented on Sep 25, 2026

    @CyrilStrone
    Contributor

    Confirming this is fixed on current main.

    I reproduced the original report with a public-API test: a pathless _authenticated route whose beforeLoad reads context.auth.isAuthenticated and throws redirect({ to: '/login' }), with auth supplied via RouterProvider context={{ auth }}. Logging out sets the auth state and calls router.invalidate() in the same event handler.

    On @tanstack/react-router@1.170.39 (fix first released in 1.170.32), beforeLoad observes the fresh context ([true, false]) and the redirect to /login happens; the inverse (login -> /private) also works.

    This was fixed by #8130 ("preserve context during reloads", merged 2026-08-20, released in @tanstack/react-router@1.170.32), which also added the issue-8115-context suite. The exact same-handler ordering (setState + invalidate() together) isn't covered by #8115, so I can open a small regression-test PR if useful - otherwise I think this can be closed as fixed.

  17. added a commit that references this issue on Sep 25, 2026
  18. added a commit that references this issue on Sep 26, 2026
  19. SiKreuz commented on Sep 29, 2026

    @SiKreuz

    I don't think this is fixed. The test above calls setState and invalidate() in the same handler. That case already worked before #8130 (it passes on 1.170.23 too). The original report has an API call before the state update. With that, on 1.170.35 the beforeLoad and loader run by invalidate() still see the old context:

    // auth comes from useState, passed via <RouterProvider context={{ auth }} />
    // _authenticated beforeLoad: if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
    await api()               // any await before the state update
    setIsAuthenticated(false)
    await router.invalidate() // beforeLoad + loader see isAuthenticated: true

    With a trailing navigate() (as in the original report) the next load picks up the new context. That masks the problem, but the context is still stale when invalidate() resolves, which is what this issue is about. Without navigate() the user stays on /, logged out. It behaves the same on 1.170.23 and 1.170.35.

    Could this be reopened?

  20. Sheraff commented on Sep 29, 2026

    @Sheraff
    Collaborator

    I don't think this is fixed. The test above calls setState and invalidate() in the same handler. That case already worked before #8130 (it passes on 1.170.23 too). The original report has an API call before the state update. With that, on 1.170.35 the beforeLoad and loader run by invalidate() still see the old context:

    // auth comes from useState, passed via <RouterProvider context={{ auth }} />
    // _authenticated beforeLoad: if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
    await api() // any await before the state update
    setIsAuthenticated(false)
    await router.invalidate() // beforeLoad + loader see isAuthenticated: true
    With a trailing navigate() (as in the original report) the next load picks up the new context. That masks the problem, but the context is still stale when invalidate() resolves, which is what this issue is about. Without navigate() the user stays on /, logged out. It behaves the same on 1.170.23 and 1.170.35.

    Could this be reopened?

    Could you PR a minimal reproduction of the issue? (kind of what #8499 did)

  21. mkarajohn commented on Sep 29, 2026

    @mkarajohn
    Contributor

    I had a pre-1.170.32 repro here: #2072 (comment)

    (open dev console once you log in, do a logout and you will see that the _authenticated route's beforeload will still have the stale context when it runs)

    I forked it and updated to 1.170.32, here: https://stackblitz.com/edit/tanstack-router-c3dgcdre?file=package.json

    if you follow the same steps, the aforementioned beforeload, has the correct context on logout now.

    I have not followed what changed internally, but as an end user it works as expected now

  22. Sheraff commented on Sep 29, 2026

    @Sheraff
    Collaborator

    I had a pre-1.170.32 repro here: #2072 (comment)

    (open dev console once you log in, do a logout and you will see that the _authenticated route's beforeload will still have the stale context when it runs)

    I forked it and updated to 1.170.32, here: https://stackblitz.com/edit/tanstack-router-c3dgcdre?file=package.json

    if you follow the same steps, the aforementioned beforeload, has the correct context on logout now.

    I have not followed what changed internally, but as an end user it works as expected now

    sorry I meant to ask for an actual PR of a test that isolates the issue

  23. SiKreuz commented on Oct 1, 2026

    @SiKreuz

    Here it is: #8575

    It adds one test next to the #8499 ones. The only difference is an awaited request before the state update, with no navigate() afterwards:

    await new Promise((resolve) => setTimeout(resolve, 10)) // logout request
    setIsAuthenticated(false)
    await router.invalidate()

    On main, _authenticated's beforeLoad sees [true, true] instead of [true, false], so the redirect to /login never happens.

    @mkarajohn your repro passes on 1.170.32 because its logout navigates afterwards, and that next load picks up the fresh context. The load triggered by invalidate() itself still sees the stale one.

  24. SiKreuz commented on Oct 14, 2026

    @SiKreuz

    @Sheraff did you have any time to look into the small repro? #8575

  25. Sheraff commented on Oct 14, 2026

    @Sheraff
    Collaborator

    31. @Sheraff did you have any time to look into the small repro? #8575

    no sorry. But looking at the example right now:

        const [isAuthenticated, setIsAuthenticated] = React.useState(true)
        return (
          <>
            <button
              onClick={async () => {
                await new Promise((resolve) => setTimeout(resolve, 10))
                setIsAuthenticated(false)
                await router.invalidate()
              }}
            >
              Logout
            </button>
            <RouterProvider
              router={router}
              context={{ auth: { isAuthenticated } }}
            />
          </>

    to me it makes sense that router.invalidate() isn't aware of setIsAuthenticated(false) since react updates it later. I don't think we should expect it to "wait for the next render when called".

  26. SiKreuz commented on Oct 21, 2026

    @SiKreuz

    Fair point: invalidate() can't know about a state update React hasn't rendered yet, and expecting it to wait for a render isn't reasonable. I'm fine treating that as by design.

    But that's the pattern the docs recommend. The React example under Invalidating the Router Context does

    auth.onAuthStateChanged((user) => {
      setUser(user)
      router.invalidate()
    })

    which runs outside a React event handler, so it hits exactly this. The authenticated-routes example works around it with await sleep(1) ("just a hack being used to wait for the auth state to update"). Maybe some doc adjustments could help here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions