Skip to content

feat[client-js]: Adds opt-in React Router loaders for @okta/okta-client-javascript - #321

Open
BenjaminTruong-okta wants to merge 4 commits into
masterfrom
feat/client-js-opt-in-support
Open

feat[client-js]: Adds opt-in React Router loaders for @okta/okta-client-javascript#321
BenjaminTruong-okta wants to merge 4 commits into
masterfrom
feat/client-js-opt-in-support

Conversation

@BenjaminTruong-okta

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

Copy link
Copy Markdown
Contributor

Adds a @okta/okta-react/client-js subpath exporting createFetchLoader, createTokenLoader, and createLoginCallbackLoader - React Router v6.4+ data-router loader factories that wrap @okta/auth-foundation, @okta/oauth2-flows, and @okta/spa-platform (0.6.0 beta) as an alternative to the okta-auth-js-based API.

createLoginCallbackLoader resumes the auth code flow via AuthorizationCodeFlowOrchestrator.resumeFlow(), which exchanges the code and stores the resulting credential itself, then redirects to the original URI. This replaces the component for apps using the new SDK's data router.

The new peer SDKs are declared as optional peerDependencies so the default @okta/okta-react bundle has no dependency on them; a dedicated Rollup target and ESLint import-boundary rule keep the two bundles isolated from each other.

Jest's jsdom environment doesn't implement a spec-compliant Fetch API, which the data router's loader/redirect handling depends on - added undici (plus its Node-native web API polyfills) via a new setupFiles entry so the loader tests exercise real Request/Response objects.

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?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Reviewers

…nt-javascript

Adds a `@okta/okta-react/client-js` subpath exporting createFetchLoader,
createTokenLoader, and createLoginCallbackLoader - React Router v6.4+
data-router loader factories that wrap @okta/auth-foundation,
@okta/oauth2-flows, and @okta/spa-platform (0.6.0 beta) as an alternative
to the okta-auth-js-based API.

createLoginCallbackLoader resumes the auth code flow via
AuthorizationCodeFlowOrchestrator.resumeFlow(), which exchanges the code
and stores the resulting credential itself, then redirects to the
original URI. This replaces the <LoginCallback /> component for apps
using the new SDK's data router.

The new peer SDKs are declared as optional peerDependencies so the
default @okta/okta-react bundle has no dependency on them; a dedicated
Rollup target and ESLint import-boundary rule keep the two bundles
isolated from each other.

Jest's jsdom environment doesn't implement a spec-compliant Fetch API,
which the data router's loader/redirect handling depends on - added
undici (plus its Node-native web API polyfills) via a new setupFiles
entry so the loader tests exercise real Request/Response objects.
Expands the client-js section to contrast the authState/React-context
model used elsewhere in this SDK with client-js's evaluate-at-point-of-use
approach, including a before/after example and concrete differences
around staleness, effect dependencies, bootstrapping, and subscriptions.

Also fixes the SDK construction example: FetchClient takes a
TokenOrchestrator as its first constructor argument, not a bare config
object, so tokenOrchestrator must be constructed first.
@BenjaminTruong-okta
BenjaminTruong-okta marked this pull request as ready for review August 12, 2026 23:58
@BenjaminTruong-okta
BenjaminTruong-okta force-pushed the feat/client-js-opt-in-support branch from 5fdc0a7 to 0aca968 Compare August 13, 2026 00:05
Base automatically changed from dependency-upgrades to master August 13, 2026 19:42
Comment thread src/client-js/createFetchLoader.ts Outdated
Comment on lines +32 to +40
export function createFetchLoader(
fetchClient: FetchClient,
getResource: GetResource,
init?: RequestInit,
) {
return async (args: LoaderArgs): Promise<Response> => {
return fetchClient.fetch(getResource(args), init);
};
}

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.

Suggested change
export function createFetchLoader(
fetchClient: FetchClient,
getResource: GetResource,
init?: RequestInit,
) {
return async (args: LoaderArgs): Promise<Response> => {
return fetchClient.fetch(getResource(args), init);
};
}
export function createFetchLoader(fetchClient: FetchClient) {
return async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
return fetchClient.fetch(url, init);
};
}

Comment thread src/client-js/createTokenLoader.ts Outdated
Comment on lines +26 to +37
export function createTokenLoader(
orchestrator: AuthorizationCodeFlowOrchestrator,
params?: TokenOrchestrator.AuthorizeParams,
) {
return async (): Promise<Token> => {
const token = await orchestrator.getToken(params);
if (!token) {
throw new Response('Unauthorized', { status: 401 });
}
return token;
};
}

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 think the "create loader" should essentially just bind to an orchestrator instance. The params can be provided to the loader itself to override at the specific route

Suggested change
export function createTokenLoader(
orchestrator: AuthorizationCodeFlowOrchestrator,
params?: TokenOrchestrator.AuthorizeParams,
) {
return async (): Promise<Token> => {
const token = await orchestrator.getToken(params);
if (!token) {
throw new Response('Unauthorized', { status: 401 });
}
return token;
};
}
export function createTokenLoader(
orchestrator: AuthorizationCodeFlowOrchestrator
) {
return async (params?: TokenOrchestrator.AuthorizeParams): Promise<Token> => {
const token = await orchestrator.getToken(params);
if (!token) {
throw new Response('Unauthorized', { status: 401 });
}
return token;
};
}

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.

I made this change, but it requires the loader to be wrapped: (loader: () => fn(...)) instead of assigning them directly because they are no longer valid react router loaders on their own

Comment thread src/client-js/index.ts
*
* See the License for the specific language governing permissions and limitations under the License.
*/

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 wonder if it makes more sense to have a createLoadersFromOrchestrator method that returns both the tokenLoader and loginCallbackLoader since they both accept the same input (orchestrator) and they probably need to interact with the same orchestrator instance to work. Then recommend this pattern is used it instead

function createLoadersFromOrchestrator (orchestrator: AuthorizationCodeFlowOrchestrator) {
  return {
    tokenLoader: createTokenLoader(orchestrator),
    loginCallbackLoader: createLoginCallbackLoader(orchestrator),
  }
}

// usage
const { tokenLoader, loginCallbackLoader } = createLoadersFromOrchestrator(orchestrator)

…all args

createFetchLoader and createTokenLoader now take only the fetchClient/
orchestrator at creation time and return a function that takes the
resource/params at call time, instead of baking those in upfront. The
returned function no longer matches React Router's loader signature
directly, so it must be called from within your own loader function.

Adds createLoadersFromOrchestrator, which binds a single orchestrator
instance to both createTokenLoader and createLoginCallbackLoader, since
they need to share that instance to see each other's stored credential.

Addresses review feedback on #321.
path: '/',
element: <Page />,
loader: createFetchLoader(fetchClient as any, () => '/api/resource'),
loader: () => fetchResource('/api/resource'),

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.

see here

@jaredperreault-okta

Copy link
Copy Markdown
Contributor

This looks good! I like the direction it's heading

I was poking around the react-router docs and found a few links that may be relevant:

It seems like react-router has 3 "modes": framework, data and declarative. Let's make sure our loader offerings work for each (if possible)
I know I told you to support v6, but it might be worth it to only support v7 and 8. The API surface area seems to be a lot cleaner, and the download stats actually don't mention 6: https://www.npmjs.com/package/react-router?activeTab=versions

@BenjaminTruong-okta

Copy link
Copy Markdown
Contributor Author

@jaredperreault-okta i think these loaders are fundamentally incompatible with declarative mode, where the <Route> has no loader property - consumers will just have to call the fetchClient/Orchestrator directly.
These loaders are built for data mode, so that works, and also no changes are required for use in framework mode, but that will require a little more app-side wiring. Added an example in the readme and can make a sample that demonstrates that in a subsequent PR.

Comment thread test/jest/setup.ts

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { fetch, Headers, Request, Response } from 'undici';

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 shouldn't need an external dependency for this. You can use the types available from node.

see https://github.com/okta/okta-client-javascript/tree/master/tooling/jest-helpers/browser

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