Search code examples
javascriptreactjsjsxfluxreactjs-flux

Flux actions getting captured by wrong handlers on this very simple dummy app, Why?


I have this very simple React app:

https://codesandbox.io/s/24oq248v4n

which is about Oranges and Lemons, just that.

It basically gets the Oranges or Lemons from an (simulated) external API, depending on the pressed button.

Below you have the code for the Oranges store (the one for Lemons is quite similar).

src\resources\assets\js\stores\OrangeStore.js

import { EventEmitter } from "events";
import { sprintf } from "sprintf-js";
import AppDispatcher from "../dispatcher/AppDispatcher";
import AppApi from "../utils/AppApi";

const CHANGE_EVENT = "change";
let _response = null;

class OrangeStore extends EventEmitter {
  constructor() {
    super();
    this.dispatchToken = AppDispatcher.register(this.handleActions.bind(this));
  }

  emitChange() {
    this.emit(CHANGE_EVENT);
  }

  addChangeListener(callback) {
    this.on(CHANGE_EVENT, callback);
  }

  removeChangeListener(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  }

  fetchOranges(data) {
    AppApi.fetchOranges(data);
  }

  setOrangeResponse(data) {
    _response = data;
  }

  getOrangeResponse() {
    return _response;
  }

  clearOrangeResponse() {
    _response = null;
  }

  handleActions(action) {
    let nameObjectClass = this.constructor.name;
    switch (action.type) {
      case "FETCH_ORANGES":
        this.fetchOranges(action.value);
        break;
      case "SET_ORANGE_RESPONSE":
        this.setOrangeResponse(action.value);
        break;
      default:
        console.error(sprintf('ATTENTION: action: "%s" entered on the wrong handle, the one for: "%s".', action.type, nameObjectClass));
        break;
    }
    this.emitChange();
  }
}

export default new OrangeStore();

Here you have the code that takes care of dispatching the actions:

src\resources\assets\js\actions\AppActions.js

import AppDispatcher from "../dispatcher/AppDispatcher";

class AppActions {
  fetchOranges(data) {
    AppDispatcher.dispatch({
      type: "FETCH_ORANGES",
      value: data
    });
  }
  setOrangeResponse(data) {
    AppDispatcher.dispatch({
      type: "SET_ORANGE_RESPONSE",
      value: data
    });
  }
  fetchLemons(data) {
    AppDispatcher.dispatch({
      type: "FETCH_LEMONS",
      value: data
    });
  }
  setLemonResponse(data) {
    AppDispatcher.dispatch({
      type: "SET_LEMON_RESPONSE",
      value: data
    });
  }
}

export default new AppActions();

Here you have the main code:

src\index.js

import React from "react";
import ReactDOM from "react-dom";
import AppActions from "./resources/assets/js/actions/AppActions";
import OrangeStore from "./resources/assets/js/stores/OrangeStore";
import LemonStore from "./resources/assets/js/stores/LemonStore";

import "./styles.css";

class App extends React.Component {

  client = {
    firstName: 'George',
    lastName: 'Washington',
  };

  componentWillMount() {
    OrangeStore.addChangeListener(this.handleOrangeResponse);
    LemonStore.addChangeListener(this.handleLemonResponse);
  }
  componentWillUnmount() {
    OrangeStore.removeChangeListener(this.handleOrangeResponse);
    LemonStore.removeChangeListener(this.handleLemonResponse);
  }

  getOranges = () => {
    AppActions.fetchOranges(this.client);
  };
  getLemons = () => {
    AppActions.fetchLemons(this.client);
  };

  handleOrangeResponse = () => {
    let response = OrangeStore.getOrangeResponse();
    console.log('inside: index.js / handleOrangeResponse() {...} | where: response == ', response);
  }
  handleLemonResponse = () => {
    let response = LemonStore.getLemonResponse();
    console.log('inside: index.js / handleLemonResponse() {...} | where: response == ', response);
  }

  render() {
    return (
      <div className="App">
        <h1>Oranges and Lemons</h1>
        <h2>Yet another Flux test!</h2>
        <button onClick={this.getOranges}>Get Oranges</button>
        <button onClick={this.getLemons}>Get Lemons</button>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

The code works almost fine but...

My problem is: the actions corresponding to the Oranges are getting captured by the Lemons listener and vice versa.

As you can see on the following image:

enter image description here

You are invited to try it by yourself on: https://codesandbox.io/s/24oq248v4n

To see the results: Info and Errors you have to open the Code Sandbox console on the bottom.

In my opinion the listeners for Oranges should capture only the Oranges actions and the same for: Lemons.

I believe I'm ommiting one key detail here.

Do you have any idea about this behavior?

If possible, please, fork my code above and provide the link of your fixed code here.

Some explanation is also welcome.

Thanks!


Solution

  • Answering my own question:

    The problem was on the store files (both of them), where I had the following code:

    src\resources\assets\js\stores\OrangeStore.js (the same applies to the other store)

    class OrangeStore extends EventEmitter {
    
      ...
    
      handleActions(action) {
        let nameObjectClass = this.constructor.name;
        switch (action.type) {
          case "FETCH_ORANGES":
            this.fetchOranges(action.value);
            break;
          case "SET_ORANGE_RESPONSE":
            this.setOrangeResponse(action.value);
            break;
          default:
            console.error(sprintf('ATTENTION: action: "%s" entered on the wrong handle, the one for: "%s".', action.type, nameObjectClass));
            break;
        }
        this.emitChange();
      }
    
      ...
    
    }
    

    Here I was doing one mistake, and it was I called: this.emitChange(); for all kind of actions, including those that are not related with the corresponding store.

    Here was the the solution.

    src\resources\assets\js\stores\OrangeStore.js (the same applies to the other store)

    class OrangeStore extends EventEmitter {
    
      ...
    
      handleActions(action) {
        switch (action.type) {
          case "FETCH_LEMONS":
            this.fetchLemons(action.value);
            // this.emitChange(); // THIS IS NOT NECESSARY HERE
            break;
          case "SET_LEMON_RESPONSE":
            this.setLemonResponse(action.value);
            this.emitChange();
            break;
        }
      }
    
      ...
    
    }
    

    Also notice that it is not necessary to call: this.emitChange() for every action, because if we do that, then the Flux mechanism will be calling the event handlers (or callbacks) unnecessary, and if we have some kind of preprocessing on those handlers that is always done before doing its main functionality, then we are going to execute that innecessary.

    When we are dealing with multiple stores, if we don't have this in mind we are going to run into issues.

    Here is the code fixed: https://codesandbox.io/s/0pml774y9l

    Here you have a quick preview with the main changes:

    enter image description here

    Interesting paragraph here:

    https://scotch.io/tutorials/getting-to-know-flux-the-react-js-architecture

    The Dispatcher is basically the manager of this entire process. It is the central hub for your application. The dispatcher receives actions and dispatches the actions and data to registered callbacks.

    So it's essentially pub/sub?

    Not exactly. The dispatcher broadcasts the payload to ALL of its registered callbacks, and includes functionality that allows you to invoke the callbacks in a specific order, even waiting for updates before proceeding. There is only ever one dispatcher, and it acts as the central hub within your application.

    Also, here there are two examples of Flux apps:

    https://scotch.io/tutorials/build-a-react-flux-app-with-user-authentication

    https://www.3pillarglobal.com/insights/getting-started-flux-react

    Pay attention where they call: .emitChange().

    Thanks!