くずきのblog

技術とか色々

ReactのEventEmitterで登録したリスナが削除できない問題の解決法

どうも、くずきです。

EventEmitterで登録したリスナが削除できない問題について解決した方法をメモしときたいと思います。

構成

  • EventEmitter
    • ErrorStore(EventEmitterを継承したクラスをStoreとして利用)
    • サーバーサイドから取得したエラーを登録したリスナに通知
  • Component
    • App.js
    • エラー時の関数をEventEmitter(Store)に登録する

EventEmitter

本記事では書いてないが、Actionでサーバー側に通信した後エラーだった場合に呼ばれるStore
this.onで通知するリスナを登録している。

class ErrorStore extends EventEmitter {
  handleActions(action) {
    if(action.type === "ERROR"){
      // エラー通知
      if(action.res.hasOwnProperty("statusCode")) {
        switch (action.res.statusCode) {
          case 500:
            break;
          case 400:
            this.emit(EVENT.ERROR.VALIDATION, action.res.body.errors);
            break;
          case 401:
            this.emit(EVENT.ERROR.AUTHENTICATION, action.res);
            break;
          default:
            break;
        }
      }
    }
  }

  addValidListener(listener) {
    this.on(EVENT.ERROR.VALIDATION, listener);
  }

  removeValidListener(listener) {
    this.removeListener(EVENT.ERROR.VALIDATION, listener);
  }

  addAuthListener(listener) {
    this.on(EVENT.ERROR.AUTHENTICATION, listener);
  }

  removeAuthListener(listener) {
    this.removeListener(EVENT.ERROR.AUTHENTICATION, listener);
  }

}

const errorStore = new ErrorStore();
dispatcher.register(errorStore.handleActions.bind(errorStore));

export default errorStore;

初期のComponent

一番初期(問題があるとき)につくったコンポーネント
handleError関数をErrorStoreaddValidListenerでリスナに登録している。

class App extends Component {

  constructor(props) {
    super();
    this.state = {
      errors: null
    };

  }

  componentWillMount() {
    errorStore.addValidListener(this.handleError.bind(this));
  }
  
  componentWillUnmount() {
    errorStore.removeValidListener(this.handleError.bind(this));
  }
  
  handleError(_errors) {
    // エラーの内容を解析する関数を呼ぶ
    let errors = Validation.getValidationErrors(_errors);
    this.setState({
      errors: errors
    });
  }


  render() {
    if (this.state.errors) { 
      ....
    }
  }
}

当初はcomponentWillUnmountで既に登録したリスナ(this.handleError.bind(this))を消すはずが

Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.

というエラーを吐き、できなかった。

EventEmitterのコードを追い、removeListenerで消す条件を確認したところ、関数同士を比較してるようだ。

実際にthis.handleError.bind(this) === this.handleError.bind(this)を行なったところfalseになり、bindした関数同士はイコールにならないみたい。
bind関数の仕様をみると、確かに関数を新たに作成してるっぽいのでこれはイコールにはならない。。。

改善版Component

ここまで分かればもう簡単で、コンストラクタでbindした関数を保持し、それを利用すれば良い。

class App extends Component {

  constructor(props) {
    super();
    this.state = {
      errors: null
    };
    
    this.handleError = this.handleError.bind(this);

  }

  componentWillMount() {
    errorStore.addValidListener(this.handleError);
  }
  
  componentWillUnmount() {
    errorStore.removeValidListener(this.handleError);
  }
  
  handleError(_errors) {
    // エラーの内容を解析する関数を呼ぶ
    let errors = Validation.getValidationErrors(_errors);
    this.setState({
      errors: errors
    });
  }


  render() {
    if (this.state.errors) { 
      ....
    }
  }
}

これでエラーも消えてめでたしめでたし。
ただjavascriptはこういったエラーを気にしないと気付かずバグをうんだりするので、もっと理解しないとなと思います。