Skip to content

feat!: Add SecureOutlet, split SecureRoute/SecureOutlet into router-specific sub-exports - #326

Open
BenjaminTruong-okta wants to merge 4 commits into
masterfrom
feat/secure-outlet-react-router-v6
Open

feat!: Add SecureOutlet, split SecureRoute/SecureOutlet into router-specific sub-exports#326
BenjaminTruong-okta wants to merge 4 commits into
masterfrom
feat/secure-outlet-react-router-v6

Conversation

@BenjaminTruong-okta

@BenjaminTruong-okta BenjaminTruong-okta commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

SecureRoute only worked with react-router-dom v5 APIs, forcing v6+ users to hand-roll their own guard component (see issues #267, #300). This PR adds SecureOutlet, which mirrors SecureRoute's auth-gating behavior using Outlet, and then splits both components into router-version-specific sub-exports so unused router code never enters a consumer's bundle.

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Adding Tests
  • Build related changes
  • CI related changes
  • Documentation changes
  • Other... Please describe:

What is the current behavior?

SecureRoute only works with react-router-dom v5 (Route-based) APIs. v6+ apps, which use Outlet-based nested routing, have no built-in guard component and must hand-roll one (#267, #300). Additionally, SecureRoute living in the top-level @okta/okta-react bundle means react-router-dom symbols it references are a latent bundler/type risk for every consumer, regardless of whether SecureRoute is used — this already caused a real production build break (#178/#187, patched reactively in #210/#213).

Issue Number: #267, #300

What is the new behavior?

  • Adds SecureOutlet, a v6+ equivalent of SecureRoute built on Outlet, with the same auth-gating behavior.
  • Splits both router guard components out of the top-level @okta/okta-react export into dedicated sub-exports: SecureRoute@okta/okta-react/react-router-5, SecureOutlet@okta/okta-react/react-router-6. Each is built as its own bundle via package.json exports, so router-version-specific code is only pulled into a consumer's build when they actually import it — closing the whole class of bug behind Support react-router v6 #178/'useRouteMatch' is not exported from 'react-router-dom' #187 rather than patching it symbol-by-symbol.
  • Ports the design from the unmerged okta/okta-react#282 (dev7) draft onto current tooling.
  • react-router-dom becomes an optional peer dependency.

Does this PR introduce a breaking change?

  • Yes
  • No

SecureRoute and SecureOutlet are no longer exported from @okta/okta-react. Consumers must update imports:

- import { SecureRoute } from '@okta/okta-react';
+ import { SecureRoute } from '@okta/okta-react/react-router-5';
- import { SecureOutlet } from '@okta/okta-react';
+ import { SecureOutlet } from '@okta/okta-react/react-router-6';

Minimum supported Node version is now 12.17.0 (required for package self-referencing via exports). Major version bump: 6.12.07.0.0.

Other information

See CHANGELOG.md and the updated README.md "Upgrading to 7.x" note for full migration details.

Reviewers

SecureRoute only works with react-router-dom v5 APIs, forcing v6+
users to hand-roll their own guard component (see issues #267, #300).
SecureOutlet mirrors SecureRoute's auth-gating behavior using Outlet,
and react-router-dom is now an optional peer dependency.
SecureRoute and SecureOutlet are v5- and v6-shaped code respectively,
but both previously lived in the same top-level @okta/okta-react
bundle/module graph. That meant react-router-dom symbols referenced by
either component were a latent bundler/type risk for every consumer,
regardless of which router version (or neither) they used - this
already caused a real production build break (#178/#187, patched in
#210/#213).

Move SecureRoute to @okta/okta-react/react-router-5 and SecureOutlet
to @okta/okta-react/react-router-6, each built as its own bundle, so
unused router-version code never enters a consumer's build graph.
Ports the design from the unmerged #282 (dev7) onto
current master's tooling.

BREAKING CHANGE: SecureRoute and SecureOutlet are no longer exported
from @okta/okta-react. Import SecureRoute from
@okta/okta-react/react-router-5 and SecureOutlet from
@okta/okta-react/react-router-6 instead. Minimum supported Node
version is now 12.17.0.
@BenjaminTruong-okta BenjaminTruong-okta changed the title feat: Add SecureOutlet component for React Router v6+ support feat!: Add SecureOutlet, split SecureRoute/SecureOutlet into router-specific sub-exports Sep 1, 2026
Comment thread src/OktaContext.ts
const OktaContext = React.createContext<IOktaContext | null>(null);

export const useOktaAuth = (): IOktaContext => React.useContext(OktaContext) as IOktaContext;
export const useOktaAuth = (context?: typeof OktaContext): IOktaContext => React.useContext(context ?? OktaContext) as IOktaContext;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you know why this is required? I get why you're doing this, just seems a bit awkward

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.

so each bundle can supply the singleton instance to useOktaAuth instead of useOktaAuth defaulting to a local OktaContext

Comment thread src/SecureOutlet.tsx
import { useOktaAuth, OnAuthRequiredFunction } from './OktaContext';
import * as ReactRouterDom from 'react-router-dom';
import { toRelativeUrl, AuthSdkError } from '@okta/okta-auth-js';
// Important! Don't import OktaContext from './OktaContext'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You should be able to add an eslint rule (like this) to prevent this

Comment thread src/SecureRoute.tsx
import { useOktaAuth, OnAuthRequiredFunction } from './OktaContext';
import * as ReactRouterDom from 'react-router-dom';
import { toRelativeUrl, AuthSdkError } from '@okta/okta-auth-js';
// Important! Don't import OktaContext from './OktaContext'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same as above, eslint rule

Comment thread rollup.config.js Outdated
const external = makeExternalPredicate();
const commonPlugins = [

// Each build below needs its own `typescript()` plugin instance (with its own cacheRoot).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I migrated away from rollup-plugin-typescript2 in okta-client-javascript. May be worth investigating (example: https://github.com/okta/okta-client-javascript/blob/master/tooling/rollup-config/sdk.mjs#L4). This explanation seems very nuanced and indicates this may be potentially fragile

rollup-plugin-typescript2 emitting .d.ts files from multiple entry-point
configs (one per sub-export) fought over writing to the same
declarationDir, requiring per-config cache roots and scratch-path
redirects to avoid TS5055. Decouple declaration emission from bundling
entirely: rollup's typescript() plugin now only transpiles, and a single
`yarn types` (tsc --emitDeclarationOnly) pass emits every .d.ts file in
one whole-program compile, matching the pattern used in
okta-client-javascript's rollup config.

skipLibCheck is added to the root tsconfig since running tsc directly
now surfaces a pre-existing duplicate @types/react conflict from
workspace hoisting that rollup-plugin-typescript2 wasn't checking.
@BenjaminTruong-okta
BenjaminTruong-okta marked this pull request as ready for review September 1, 2026 18:56
Add a no-restricted-imports override blocking the default OktaContext
export from './OktaContext' in these two files, so the "import from
@okta/okta-react instead" requirement is enforced by lint rather than
relying on a comment being followed.
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