With Clerk, logOut triggers a React error

I’m using Clerk for authentication, and when I call the logOut method from the useAuth hook, I see a React error in my browser console:

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
at AuthProvider

Has anyone else encountered this issue?

I think this code might be triggering the error, but I’m not 100% sure. Perhaps the AuthProvider component should check whether it’s still mounted before calling setState, as demonstrated here?

Interesting. We have Clerk setup in the Auth Playground here https://redwood-playground-auth.netlify.app/

I don’t think I’ve seen that but maybe it’s due to where we have the logout component — or not using the Clerk components?

Could you share how you have the logout used in a page or company so we can reproduce and see a change is needed in the provider or we can also get Clerk to help if a different approach is needed to correct.

Thanks for the quick response, @dthyresson!

Here’s a simplified version of my App.js and the Header component with my logout button. If I can provide any other information that would be helpful, please let me know.

App.js

// I didn't modify this much after running `yarn rw setup auth clerk`.
import { ClerkProvider, withClerk } from '@clerk/clerk-react'
import { AuthProvider } from '@redwoodjs/auth'
import { FatalErrorBoundary, Head, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'

import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'

import './index.css'

const ClerkAuthConsumer = withClerk(({ children, clerk }) => {
  return React.cloneElement(children, { client: clerk })
})

const ClerkAuthProvider = ({ children }) => {
  const frontendApi = process.env.CLERK_FRONTEND_API_URL
  if (!frontendApi) {
    throw new Error('Need to define env variable CLERK_FRONTEND_API_URL')
  }

  return (
    <ClerkProvider frontendApi={frontendApi}>
      <ClerkAuthConsumer>{children}</ClerkAuthConsumer>
    </ClerkProvider>
  )
}

const App = () => (
  <FatalErrorBoundary page={FatalErrorPage}>
    <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
      <ClerkAuthProvider>
        <AuthProvider type="clerk">
          <RedwoodApolloProvider>
            <Head>
              {/* Stylesheet links for Google fonts */}
            </Head>
            <Routes />
          </RedwoodApolloProvider>
        </AuthProvider>
      </ClerkAuthProvider>
    </RedwoodProvider>
  </FatalErrorBoundary>
)

export default App

Header.js

import { useAuth } from '@redwoodjs/auth'
import { Link, NavLink, routes } from '@redwoodjs/router'
import logo from './transparent_logo.png'

const HeaderButtons = () => {
  const { isAuthenticated, loading, logOut } = useAuth()
  if (loading) {
    return null
  }
  if (isAuthenticated) {
    return (
      <button
        onClick={async () => {
          await logOut()
        }}
        type="button"
      >
        Sign Out
      </button>
    )
  }
  return (
    <div>
      <NavLink
        activeClassName="hidden"
        to={routes.signIn()}
      >
        Sign In
      </NavLink>
      <Link
        to={routes.signup()}
      >
        Sign Up
      </Link>
    </div>
  )
}

const Header = () => {
  return (
    <header>
      <Link to={routes.home()}>
        <img alt="logo" src={logo} />
      </Link>
      <HeaderButtons />
    </header>
  )
}

export default Header

Oh the “withClerk” I think has changed.

See: playground-auth/Clerk.js at main · redwoodjs/playground-auth · GitHub

And also

That changed recently as Clerk gets closer to implementing a v2 of auth.

Can you try those imports instead and see if things behave better?

Thanks for the tip, @dthyresson!

I took another look this morning, and I realized that the React error appears only in my local environment, not on my Vercel preview deployment.

Following your advice, I switched from withClerk to useClerk (and upgraded @clerk/clerk-react for good measure). I still see the React error on my local but not on the preview deployment.

I’m still not 100% what the root issue is, but since it appears to be happening only locally, I’m not too worried.

Ok, good to know. I will ask a few others to check this and see if they have any ideas.

Thanks!

It’s happening on Vercel too, it’s just that React only shows that error message in dev.

And I’m getting the same error now that I switched my Clerk config from passwordless/magiclink to email+pw

Ah, I didn’t realize that React error messages show up only in dev - thanks for correcting me!

And good idea to check the authentication method. I’m also using email+pw.

Heyo - I wrote the original Clerk integration and also ran into this in a project recently so swinging by to see if I can help.

The Bug:

This definitely is a bug. The pieces I’ve identified that are causing this are:

  1. According to their docs, the clerk-js library deletes and recreates the Clerk client as part of logout. (Side note, don’t love this. Would much rather they could just modify the state in place?)
  2. The withClerk HOC and WithClerk component both unmount their children when the Clerk client is not loaded, both at first load and upon sign-out.
  3. The AuthProvider sign-out waits on the sign-out process and then calls setState on the instance of AuthProvider that initiated the signout.

Because of these three pieces interacting, we are seeing this flow where a sign-out deletes the clerk client, which unmounts the App tree below ClerkProvider, which unmounts the first AuthProvider instance. It then makes a new clerk client, which mounts a new App tree, which mounts a second AuthProvider instance. The promise to signout then resolves, which causes the first AuthProvider to try to update its state, which causes this error.

Thinking about a solution:

I had hoped that moving to useClerk in Clerk’s v3 sdk would insta-fix this since that version allows the useClerk hook to return a nullish value if the SDK isn’t loaded rather than throwing. However, this causes an issue where now we’re not reloading enough - since the tree is no longer unmounted, we don’t reload when Clerk’s client reloads or the user state changes. I could fix that by disabling Clerk’s modal-style auth, but that is a rough user experience IMO.

I did finally get it working by doing the above changes and then adding a listener below the AuthProvider that triggers Redwood’s reauthenticate if it detects that Clerk’s auth state has changed. This is as kludgy in terms coding, but does preserve a smoother user experience. Here’s the final App.tsx I wound up with as a github gist. Would appreciate comments on this approach before I PR it as a tweak to the setup-clerk template.

1 Like

Thank for driving this forward Zack!

That’s a lot of code to be putting in a user’s App.tsx. If we could tuck most of that away inside @rw/auth though, and just import from there I think this could be a nice solution :slight_smile:

Agreed @tobbe and I will take some time this week to sketch out a PR for that.

One thing for discussion: I worry that the more we insert code into the rw/auth package the more we approach a point where we should break that into rw/auth-core and rw/auth-.

E.G. here where we start to really depend on a particular version of clerk-react, we’d want to specify that as a dep or peer dep for people who were using the Clerk auth. However, package.json doesn’t give us a way to specify “conditional” dependencies, like “if you’re using clerk, it has to be this version. if you’re using auth0, this version.” So, we wind up with a choice of: a) a mysterious failure if the end user upgrades their clerk/auth0/whatever outside of our support scope (current situation), b) having to specify all providers as dependencies no matter which you use (non-starter imo), or c) breaking the provider integrations into separate packages so you import only the one(s) you need.

Is this something the core team would be against or is it more in the “if we have time to do this safely that would be fine” category of Big Ideas?

We want to break all the auth provider integrations out in separate packages. Ideally those packages should be maintained by the auth providers themselves, not us in the core team.

After RW v1 is out we want to sit down and think about supporting plugins in RW, where not only auth can be supported as plugins, but also stuff like tailwind/chakra, and more.

Makes sense and would be happy to chip in on the plugins after 1.0.

Until then, have my PR done to fix this: Fix clerk issues with early mounting and unmounting by zackdotcomputer · Pull Request #4880 · redwoodjs/redwood · GitHub. It does rely on Clerk’s alpha v3 sdks, so I don’t think we should consider merging it until they release those versions, but I would appreciate a look over all the same.