Skip to content

Fix auth router initialization stability#184

Merged
IzumiSy merged 15 commits intomainfrom
fix/auth-router-stability
Apr 20, 2026
Merged

Fix auth router initialization stability#184
IzumiSy merged 15 commits intomainfrom
fix/auth-router-stability

Conversation

@IzumiSy
Copy link
Copy Markdown
Contributor

@IzumiSy IzumiSy commented Apr 17, 2026

Summary

  • stabilize the auth flow around the OAuth redirect path
  • prevent auth-driven rerenders from causing duplicate initialization work for the same location
  • move normal auth startup responsibility into AuthProvider
  • avoid callback-time races between callback handling, auth initialization, and auto-login
  • add regression coverage for the affected auth and router flows
  • add a patch changeset for @tailor-platform/app-shell

Background

This branch fixes an auth flow bug around the OAuth redirect path.

Two problems were overlapping:

  1. The router could be recreated for the same location during auth-driven rerenders. When that happened on an OAuth callback URL, initialization work for that location could run more than once, which is risky for authorization code flows because the callback code is single-use.
  2. Normal auth initialization and auto-login could overlap with callback handling. During the callback window, that made the app vulnerable to inconsistent rendering or login bounce behavior depending on whether the tree was guarded and when auth state became ready.

The fix moves auth ownership into AuthProvider and makes callback progress explicit. AuthProvider now owns the normal mount-time auth check, skips that path while the current URL is still an OAuth callback, and reads a dedicated callback-status store to decide when unguarded children should render. Guarded trees continue to rely on auth state, but callback handling is coordinated by the same provider-level flow.

On the router side, the router instance is memoized so auth state changes do not recreate it for the same route configuration. The old root-route auth context is removed, leaving the router responsible only for navigation loading, route creation, and rendering.

Together, these changes make the post-login path deterministic and remove the callback/initialization races that were leaving the app in an inconsistent state.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 17, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@tailor-platform/app-shell@184
npm i https://pkg.pr.new/@tailor-platform/app-shell-vite-plugin@184

commit: 0120355

@IzumiSy
Copy link
Copy Markdown
Contributor Author

IzumiSy commented Apr 17, 2026

/review

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by API Design Review for issue #184

Comment thread packages/core/src/contexts/auth-context.tsx Outdated
IzumiSy added 7 commits April 19, 2026 17:50
This reverts commit 4bd795a.
… up EnhancedAuthClient interface

- useEnsureAuthInitialized now accepts callbackStatus as a parameter and
  only skips initialization while the callback is 'pending'; a 'rejected'
  status falls through so checkAuthStatus can still resolve the session
- getCallbackStatusSnapshot and subscribeCallbackStatus made optional on
  EnhancedAuthClient so consumers who mock or implement the interface do
  not need to provide internal callback-tracking methods
- useCallbackStatus uses optional chaining with 'idle' fallback for
  clients that do not supply the optional methods
- update createAuthClient JSDoc to document the OAuth callback side effect
- update guardComponent JSDoc to reflect that AuthProvider renders the
  guard directly (no router dependency)
- add tests: rejected callback triggers checkAuthStatus, createAuthClient
  calls handleCallback on OAuth URL and skips it on plain URLs
@IzumiSy
Copy link
Copy Markdown
Contributor Author

IzumiSy commented Apr 19, 2026

/review

@IzumiSy
Copy link
Copy Markdown
Contributor Author

IzumiSy commented Apr 20, 2026

No API changes, so API design review emits noop. Good to go.

Copy link
Copy Markdown

@interacsean interacsean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think good @IzumiSy - a couple of AI review notes, but nothing a blocker

@@ -81,13 +151,41 @@ export interface EnhancedAuthClient extends AuthClient {
export function createAuthClient(config: AuthClientConfig): EnhancedAuthClient {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback from Claude:

Side effect inside createAuthClient (auth-context.tsx:151-170). Constructing a client now fires a network request if the URL has ?code/?error. The docstring warns about it, but this makes the factory non-idempotent — two calls during HMR, StrictMode double-invoke, or a misconfigured test setup would each attempt a single-use code exchange. Consider either (a) gating with a module-level "already started" flag keyed on the URL, or (b) moving the auto-start into a dedicated startCallbackIfPending() method the consumer calls explicitly. At minimum, a StrictMode test proving it's safe would be reassuring.

I think it's probably ok to let the caller manage only calling this function once.

However, it's probably fair and valid to try to be protective of misuse, given this is a library and we want to minimize errors on the consumer-side, even if they are from incorrect usage. So thought I will mention it anyway

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StrictMode concern doesn't apply here — createAuthClient is designed to be called at module scope, outside the React lifecycle entirely. StrictMode's double-invocation only affects components, hooks, and effects, not module-level code. For HMR, a developer actively editing source files while sitting on an OAuth callback URL (?code=...) isn't a realistic scenario in practice. The JSDoc already documents this as an intentional side effect that runs before any React render. No change planned for this.

}),
[configurations.modules, configurations.settingsResources, rootComponent, props.rootGuards],
);
const routes = useMemo(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review:

routes memo depends on children (router.tsx:87-99). The useMemo for routes (and transitively router) includes children in its deps. If callers pass a fresh JSX tree each render (common — e.g. ), the memo never hits, defeating the stated stability goal. The stability assertion in router.test.tsx uses

Home
inline in renderWithConfig, which is a stable reference within a single render but not across auth-driven reparenting. Worth verifying in an integration-level test that simulates an auth state change while children is a fresh fragment.

I'm unsure if this is valid based on usage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid concern. In practice, children passed to RouterContainer from AppShell includes <BuiltInCommandPalette /> which is a fresh JSX reference every render, so the routes memo never hits and the router ends up being recreated more than intended. This is being addressed in #185, which moves shell rendering and the auth guard wrapper out of the route definition path into a separate internal React context consumed at render time. That PR is a direct follow-up to this one.

children,
}),
] satisfies Array<RouteObject>,
[configurations, contentRoutes, children],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configurations could also easily not be memoized and therefore bypass the impact of useMemo.

Context values are only stable if the provider memoizes them. useContext returns whatever was last passed to

I don't think there's anything efficient we can do about it if the consumer is passing in a fresh object every time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current architecture, configurations is stable in practice because AppShell already memoizes the context value via useMemo before passing it to AppShellConfigContext.Provider. Any consumer going through the intended API receives a stable reference. Agreed there's no efficient fix if someone constructs a fresh object and passes it directly to the Provider, but that's outside the supported usage pattern.

@IzumiSy IzumiSy merged commit b5e4352 into main Apr 20, 2026
4 checks passed
@IzumiSy IzumiSy deleted the fix/auth-router-stability branch April 20, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants