React componentWillUpdate throwing errors


#1

Hey guys,

I’ve got myself into a bit of a mess here.

I’ll briefly explain what I’m doing first.
I have a basic Sidebar, Main and Footer layout.
Links can be in the Sidebar or the Main areas but will all render into the Main component.
I’m using React Router v4 for all the routing and not using Blaze at all, everything is React.
I have basic user profile pages set up which display the users info and each profile page is accessed via a url like this : example.com/username.

In the UserProfile template I access the required user’s data based on the url parameter (/username).

This is how I am getting it in the component, setting the data to state.

constructor(props) {
    super(props);

    this.state = {
      gotData: false,
      profile: {
        active: false
      }
    }

    this.findUser = this.findUser.bind(this);
    this.findUser(this.props.match.params.username);
  }

  componentWillUpdate() {
    if (this.props.match.params.username) {
      this.findUser(this.props.match.params.username);
    }
  }

  componentWillUnmount() {
    this.setState({
      gotData: false,
      profile: {
        active: false
      }
    });
  }

  findUser(username) {
    let local = this;
      Meteor.call('findUserProfile', username, function(err, res){
        if (res) {
          local.setState({
            gotData: true,
            profile: {
              active: true,
              pid: res.pid,
              username: res.username
            }
          });
        } else {
          local.setState({
            gotData: true,
            profile: {
              active: false
            }
          });
        }
      });
  }

The problem I’m having is the following :

When I move from a user profile link (/username) to a static page (/home) everything works as expected but I am getting the following error in the console :

Warning: Can't call setState (or forceUpdate) 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.
    in UserProfile (created by ReactMeteorDataComponent)
    in ReactMeteorDataComponent (created by Route)

If I remove the componentWillUpdate function the error goes away but the user profile pages never update with new user data if the UserProfile component is already mounted. If you are coming from a non-profile url like (/home) which uses a different component they will work though.

So I need to use componentWillUpdate to ensure the user data is re-rendered if the user data has changed while the UserProfile component is mounted.

I understand why the error is being thrown (as it’s trying to set.State on the now unmounted component when navigating to a non UserProfile page) but I don’t know how to work around it in this instance. Any help greatly appreciated.


#2

Why are you not simply using react-meteor-data?


#3

Still learning here. Just trying to cobble it together as best I can.

I have withTracker running on this component for the current user but not sure how I would use it in this instance?


#4

Is there something I can use like componentIsMounted that will allow me tell if a component is actually mounted?

I know that doesn’t exist but is there anyway of putting a conditional in the componentWillUpdate function that will only run if the component is being loaded?

Every way I’ve tried it always still runs the findUser function when I navigate to a non UserProfile page.


#5

If you want to display userdata your components should look like this:

import React from 'react';
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';

class Example extends React.Component {
   render() {
      return (
         <div>{this.props.user.username}</div>
      )
   }
}

export default ExampleContainer = withTracker((props) => {
   const specificUserHandle = Meteor.subscribe('oneUser', props.match.params.id);
   const user = Meteor.users.findOne({_id: props.match.params.id});
   return {
      user: specificUserHandle.ready() ? user : { username: '' }
   }
})(Example);

And then you publish this server-side

Meteor.publish("oneUser, function(id) {
   if(isAdmin_StubPropertyToBeReplacedByYourOwn) {
      return Meteor.users.find({_id: id});
   }
}

This will:
a) Publish only the userdata you want to the client. I’d only use Meteor.call() if you want to change data. If you just want to display something, you should work with Publish/Subscribe because otherwise it will be only static until you refresh the page.
b) The withTracker component then wraps around your actual component. It receives the original props, does stuff with it and then returns the same props plus the ones you return in the wrapper as props to your component. It will automatically refresh when something changes with the data.

This here:

user: specificUserHandle.ready() ? user : { username: '' }

is to make sure that you don’t run into problems because undefined has no property username or something. Because it’s likely that the first run of Meteor.users.find() will yield an undefined result because the subscription lags a bit. It will automatically re-run, though, and then return the actual data.


#6

That worked perfectly thank you.

Also thanks for the clarity on when and when not to use Meteor.call. That’s helped a lot.