Two times called method. ¿Why?

Hi

I made a method to change email. The code in the client is (react)

componentWillUpdate(nextProps, nextState) {
        if (nextProps.saveChanges) {
            // Recopile data
            // email = this.refs.email.value;
            // Call to Method
            Meteor.call('users.update', {
                email: this.refs.email.value
            }, (err, res) => {
                if (err) {
                    if (err.error == "validation-error") {
                        toastr.error('Insert valid email', 'Error');
                    }
                    else if (err.error == "email-exist") {
                        toastr.error('The new email is the same', 'Error');
                    }
                    else {
                        toastr.error('Unknown error', 'Error');
                    }
                    console.log(err);
                }
                else {
                    // Success
                    toastr.success('Please check your email for validation instructions.');
                }
            });
        }
    }

Now the code of the method (Reader for the client and server in a shared location)

Meteor.methods({
    'users.update'({ email }) {
        new SimpleSchema({
              email: {
                type: String,
                regEx: SimpleSchema.RegEx.Email
            }
        }).validate({ email });
        // Chequear si es el mail anterior
        var user = Meteor.user();
        var userId = this.userId;

        if (email == user.emails[0].address) {
            throw new Meteor.Error("email-exist");
        }
        if (Meteor.isServer) {
            Accounts.removeEmail(userId, user.emails[0].address);
            Accounts.addEmail(userId, email);
            Accounts.sendVerificationEmail(userId);
        }
    }
});

The problem is. If i change the email in my view the method is executed TWO TIMES. First with the success message and the second time with the same mail error.

Why the method is two times called?

Thank you

I’m almost 100% sure the problem is that you’re running one-off code inside a function (componentWillUpdate) which can be called several times unknowingly. Do not perform method calls in componentWillUpdate, it’s a severe anti-pattern. The reason is that componentWillUpdate will be called when the props or state of the component changes, and results in a re-render. Coupling your method calls to the rendering lifecycle is not a good idea. Instead, create a new method on the component called _willUpdateEmail, and then call this on user input. That way, you can guarantee the user input and the side effect are in one-to-one correspondence.

You haven’t provided the code to entire component or the toastr function, but I’m guessing that your toastr function is updating the state in order to show a message, which results in a re-render, which results in the method being called again with an undefined this.refs.email.value. If this is not the case, then you’re most likely sending the user object down through props to this component, which causes it to get updated when DDP sends a changed message down to the client, which causes componentWillUpdate to fire again.

Solution: Don’t use componentWillUpdate to make RPC calls.

It’s not possible because i have the button in a Title component called by the page layout.

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

    render() {
        return(
            <div>
                <Title title="Account settings" button="savechanges" save={() => {this.setState({saveChanges: true})}}/>
                <div className="ui two column container grid">
                    <Profile userData={this.props.userData} saveChanges={this.state.saveChanges}/>
                </div>
            </div>
        );
    }

}

So. When i click the button the props.savechanges change from false to true and i activate the email change. This works okey because i tested componentWillUpdate with an alert(‘Hello’) and a console.log(‘hello’) and its only executed one time when i click the button.

Edited 20 characterers

Maybe use a container? I used to have all these problems until I switched to a container. Basically there would be a delay in a collection being received from the DB, but to a component this looks like a refresh so it ran methods twice…

That way, even if you use componentWillUpdate, the data should be consistent. To streemo’s point though, this is anti-pattern and not a good plan to begin with. The problem then becomes how to tell a component something has changed and do something. I use MobX for this, but your mileage may vary. Smarter people than me use stores to manage this functionality. Just a thought.

Okey okey. I removed from componetWillUpdate

changeEmail() {
        Meteor.call('users.update', {
            email: this.refs.email.value
        }, (err, res) => {
            if (err) {
                if (err.error == "validation-error") {
                    toastr.error('Insert valid email', 'Error');
                }
                else if (err.error == "email-exist") {
                    toastr.error('The new email is the same', 'Error');
                }
                else {
                    toastr.error('Unknown error', 'Error');
                }
            }
            else {
                // Success
                toastr.success('Please check your email for validation instructions..');
            }
        });
    }
    render() {
        if (this.props.saveChanges) {
            this.changeEmail();
        }
        return(
            <div className="column">
              ........

Same mistake. Two times method when i change email (Succesful change and repeated email).

Are you absolutely sure that this.props.userData isn’t changing whilst
this.state.savechanges is true? If you’re sure the code in
componentWillUpdate is only getting called once, maybe your meteor stub is
affecting the state, as well?

Putting the method call in the render is no different than putting it in componentWillUpdate. Your problem is that you are not tying user-driven actions to your method call, you are tying it to the component life-cycle which is a really bad design practice, and leads to all sorts of problems including functions running more times than you expect…

Okey. Where i can put the changeEmail trigger?

Is there any chance you have put this method somewhere odd? Is it in client or server? I notice you have said it is on a shared folder in another thread.

If you are using this, it should only be on server. I’m not entirely sure what happens if it is run in a shared folder.

I would setup a helper, changeEmail() somewhere on client, which does a meteor.call to the server (users.update) and ensure the Accounts.whatever are only on server or client depending on what you are trying to do. You kind have done that already, but if it is a shared folder it will be cranky i think. Thanks so much.

Tat

The way you’ve designed your method call to be triggered is somewhat brittle. I would suggest not making it call depending on state – instead have a method called _submitChanges which is called onClick of the button. There’s no need to tie this into state or props

Meteor guide: https://guide.meteor.com/methods.html

Here’s how you can use the built-in Meteor.methods API
to define a Method. Note that Methods should always be defined in
common code loaded on the client and the server to enable Optimistic UI

So. I put a Meteor.isServer checker ( Accounts.setPassword not working [Solved] )

I don’t use submitchanges because the button is not in the form. Is on another component in page layout called “Title”. You can see the code in the first posts

FIXED!!

The problem was: The userData is REACTIVE. So, when in my method i execute:

...
if (Meteor.isServer) {
            Accounts.addEmail(userId, email);
            Accounts.removeEmail(userId, user.emails[0].address);
            Accounts.sendVerificationEmail(userId);
        }
...

The component updates because i’m altered the user profile.

Solution? In the page layout:

// Called by ../layouts/settings_layout (by Routes)
import React from 'react';

// Componentes
import Title from '../../components/settings/title';
import Profile from '../../components/settings/profile';
class SettingsIndex extends React.Component {
    constructor(props) {
        super(props);
        this.state = {saveChanges: false};
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.userData !== this.props.userData) {
            if (this.state) {
                this.setState({saveChanges: false});
            }
        }
    }

    render() {
        return(
            <div>
                <Title title="Configuración de la cuenta" button="savechanges" save={() => {this.setState({saveChanges: true})}}/>
                <div className="ui two column container grid">
                    <Profile userData={this.props.userData} saveChanges={this.state.saveChanges}/>
                </div>
            </div>
        );
    }

}

export default SettingsIndex;

componentWillReceiveProps did the trick.

Thanks !

1 Like