Meteor.logout() causing memory leak in React

I posted this on StackOverflow, however, there’s still no solution.

When I logout of my meteor app I get memory leak error. It seems to happen about 50% of the time and I can’t figure out what I’m doing wrong here. Can someone please explain what’s wrong with my method. I think it has something to do with the way I’m controlling the authenticated routes.

Error message

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.

App details
Metoer, React, React-Router V4

Path: LogoutButton.jsx

class LogoutButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      logoutRedirect: false
    };

    this.handleLogout = this.handleLogout.bind(this);
  }

  handleLogout = e => {
    e.preventDefault();

    Meteor.logout(err => {
      if (err) {
        console.log('err', err);
      } else {
        this.setState({ logoutRedirect: true });
      }
    });
  };

  render() {
    const logoutRedirect = this.state;

    if (logoutRedirect.logoutRedirect) {
      return <Redirect to="/" />;
    }

    return (
      <button
        type="button"
        className="btn btn-link dropdown-item text-dark"
        onClick={this.handleLogout}
      >
        <FontAwesomeIcon icon={faSignOutAlt} className="mr-2 text-dark" />
        Logout
      </button>
    );
  }
}

Path: IsAuthenticatedRouteNavbar

function IsAuthenticatedRouteNavbar() {
  return (
    <nav>
      <div className="collapse navbar-collapse" id="navbarSupportedContent">
        <ul className="navbar-nav ml-auto">
          <li className="nav-item">
            <NavLink className="nav-link" exact to="/">
              Home
            </NavLink>
          </li>
          <li className="nav-item">
            <LogoutButton />
          </li>
        </ul>
      </div>
    </nav>
  );
}

Path: IsAuthenticatedRoute.jsx

const IsAuthenticatedRoute = ({ component: Component, ...rest }) => {
  return (
    <Route
      {...rest}
      render={props => {
        const { loading, profile } = rest;

        const isAuth = profile && profile.isAuth;

        if (loading) {
          if (isAuth) {
            return (
              <React.Fragment>
                <IsAuthenticatedRouteNavbar /> <Component {...rest} />
              </React.Fragment>
            );
          }

          return (
            <p className="text-center pt-5 mt-5">
              You don't have the right account to access this page. Please return to your{' '}
              <Link to="/login">dashboard</Link>.
            </p>
          );
        }
        return <span>...</span>;
      }}
    />
  );
};

const IsAuthenticatedRouteContainer = withTracker(props => {
  const profileHandle = Meteor.subscribe('publish-user-profile');
  const loadingProfileHandle = !profileHandle.ready();
  const profile = Profiles.findOne({ userId: Meteor.userId() });
  const loading = !loadingProfileHandle;

  return {
    loading,
    profile
  };
})(IsAuthenticatedRoute);

Path: PublicNavbar

function PublicNavbar(props) {
  const { isAdmin, isLoggedIn } = props;

  return (
    <nav>
      <div>
        <ul className="navbar-nav ml-auto">
          {isLoggedIn ? (
            <React.Fragment>
              {isAdmin ? (
                <React.Fragment>
                  <DropdownItem>
                    <NavLink className="nav-link" to="/admin/accounts">
                      Accounts
                    </NavLink>
                  </DropdownItem>
                </React.Fragment>
              ) : null}

              <LogoutButton />
            </React.Fragment>
          ) : (
            <li className="nav-item">
              <NavLink className="nav-link" exact to="/login">
                Login
              </NavLink>
            </li>
          )}
        </ul>
      </div>
    </nav>
  );
}

const PublicNavbarContainer = withTracker(() => {
  let loading;
  let profile;

  if (Meteor.isClient) {
    const profileHandle = Meteor.subscribe('publish-user-profile');
    const loadingProfileHandle = !profileHandle.ready();
    profile = Profiles.findOne({ userId: Meteor.userId() });
    loading = !loadingProfileHandle && !!profile;
  }

  return {
    loading,
    isAdmin: profile && profile.isAdmin
  };
})(PublicNavbar);

Path: IsPublicRoute

const IsPublicRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={props => {
      let isLoggedIn = false;
      if (Meteor.isServer) {
        isLoggedIn = props && props.staticContext && props.staticContext.checkIfUserIsLoggedIn;
      }
      if (Meteor.isClient) {
        isLoggedIn = Meteor.userId() !== null;
      }

      return (
        <React.Fragment>
          <PublicNavbar isLoggedIn={isLoggedIn} />
          <Component {...props} />
        </React.Fragment>
      );
    }}
  />
);

That error occurs when you run this.setState on a component that isn’t mounted in your application.

I can’t see the code where you mount your LogoutButton, but I’m going to assume it looks something like

// snip
render() {
  return(
    <div>
      { !Meteor.userId && <LogoutButton /> }
   </div>
  )
}
// snip

If this is the case, then it's likely that this component is re-rendering before the `Meteor.logout` callback is fired. In this case, your LogoutButton will not longer be mounted, and the callback will be run on an unmounted component.

To get around this, change your conditional rendering of the logout button to use your own state, and have the logout callback modify that state.
2 Likes

Ok, I think I understand what you’re saying here, however, I can’t seem to figure out how to implement it. I use a <LogoutButton /> component because I have more than one type of Authenticated Navbar. I’ve updated the example to give you a complete view. Sorry about the length, can you show me how you’d go about setting this up. I’ve tried a few different methods but none of them worked.