bjerkio/oidc-react

Question: How to work around useAuth + "AuthContext cannot be undefined" error in SSR situation

pseudoramble opened this issue · 7 comments

In v1.5.1, there was a change introduced here to check that AuthContext is defined: #576. It's a reasonable change, but it causes an issue for us. We explicitly avoid rendering these components during SSR because there's no window object, and therefore the library won't handle it.

The code is approximately this:

// Near the root component of the app
if (typeof window !== 'undefined') {
  return <AuthProvider {...oidcConfig}>{children}</AuthProvider>
}
return children

This worked because before 1.5.1 useAuth didn't require the AuthContext to be defined. Now that it does require it this won't work anymore. I cannot optionally call useAuth because that will break the rules of hooks and cause errors from React itself.

Any thoughts? I'm happy to adjust my approach to accommodate the change, but I'm not sure what that adjustment would even be.

It's causing an issue for us as well, we have a setup where OIDC authentication is optional (they can either use a pre-shared token, or use OIDC but not both). This is defined at the application level.

We previously weren't rendering the <AuthProvider> at all in "token" mode, but useAuth() now throws an error (understandably). We cant render an <AuthProvider> with no props due to this change

An option I've used, similar to a situation I've found in a previous iteration with the Apollo provider (https://www.apollographql.com/docs/react/development-testing/testing/#the-mockedprovider-component), was to create a render method that could wrap the component being tested in an <AuthProvider> component.

So in my shared test utils index.tsx I have:

export function renderWithAuthProvider(ui: React.ReactElement): RenderResult {
    const oidcOptions: AuthProviderProps = {};
    return render(
        <AuthProvider {...oidcOptions}>
            {ui}
        </AuthProvider>
    );
}

For simplicity's sake (and because I haven't touched the code base in a while and am rusty with TS) I didnt pass in any options and just set them to a default object, but I'd imagine you could pass in a mock specific to the test if needed.

As for the test itself, this will now pass:

test("renders the header", () => {
    const { container } = renderWithAuthProvider(<Header />);
    expect(container).toBeInTheDocument();
});

Thanks for the tip @MiguelAlho but I'm not sure I follow. Are you suggesting a chance to oidc-react to create another render method that can deal with the situation?

The issue I'm facing is that the underlying oidc-client-js library requires that window is defined. To work around that, I would simply not render the AuthProvider until I knew window was defined. That worked until the context was required to be defined. I guess another way around it is I could add params to useAuth to accept if it's doing SSR, and not fail in that situation. Might be a good change to make anyway.

not really - my suggestion was from a consumer point-of-view - (someone writing tests on a component that uses useAuth). It worked for me (partially) so that's why I suggested it as a possible solution. I unfortunately hadn't considered the SSR aspect of this.

Also, reagrding the wrapping with AuthProvider, there are some UnhandledPromiseRejectionWarnings showing up for me now, potentially due to how I've configured the options. In this case, i get Error: No authority or metadataUrl configured on settings in the console (but doesn't fail the test), so my suggestion might not even be a valid solution.

I've come up with a potential solution to the issue. I genuinely don't know if I like it or not. It consists of two parts:

  1. Make AuthContext never be in an undefined state.
  2. Add an ssr flag to useAuth.

First a default AuthContext is created that says oidc-react is loading, and plugs in dummy values everywhere else. This works because once the actual AuthProvider is run, it will replace it with a legit AuthContext to be used later in the system.

Second, in useAuth it accepts an ssr flag, which simply says if it's server-side or not. Then there are a few cases:

  1. ssr = true, context = defaultContext => returns defaultContext. Since effects are not run server-side, none of the extra auth code will be running.
  2. ssr = false, context = defaultContext => throws error because we haven't loaded the proper AuthContext yet.
  3. ssr = true, context != defaultContext => returns context. In the case that something useful for SSR is given in the future, it would be valuable to have the real context. Right now though oidc-client and thus oidc-react depend on running client-side, so this information would not be valuable.
  4. ssr = false, context != defaultContext => returns context since this is a valid state.

I have a demo I opened up to review it. Again, not really sure about it, but it does seem to work for me.

For now I'm going to use this change in my projects and see how it fares. I'll update and make the PR official when I feel confident with it.

Hey!

I'm closing this for now, but feel free to open a new issue or let me know if you feel this should be re-opened!