Search code examples
reactjsstateflux

Why is my application state updating without change event from store - flux


TL;DR
Here's a video of me explaining (poorly) the issue:
https://www.youtube.com/watch?v=lJogEfYYHRo&feature=youtu.be

I'm making a chat feature on a web application using flux. I have a store with an array called messages. As is expected with flux whenever there is a change to the store, the store emits a change event and a listener on the view hears it and updates the state using a getter method from the store.

In this application I want to load some initial messages ( to imitate grabbing a history of messages from a database ). For some reason however, whenever I set the state on my view to the store getter ( which returns the array of messages ) it continues to update whenever a new message is pushed to the messages array in the store, which it shouldn't. It should only update when a change event is broadcasted. Why is this the case?

Here's The View & All Relevant Code

class ChatBar extends React.Component {

constructor(props) {
    super(props);
    // CHAT FUNCTIONS
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this._onChange = this._onChange.bind(this);

    this.state = {
        chat__messages: [],
        chat__playercount: 0,
        chat__message_input: "",
    };
}

// ====================================================

componentDidMount(){
    ChatStore.on(CHANGE, this._onChange);
    ChatStore.init();    
}

componentWillUnmount() {
    ChatStore.removeListener(CHANGE);
}

_onChange() {
    // IN THE FUTURE NEED TO CHANGE THIS TO 
    // RETURN ENTIRE STATE, NOT JUST MESSAGES
    this.setState({chat__messages: ChatStore.getMessages()});
}

// HANDLING OF FORMS, DATA SUBMISSION ECT
// =====================================================
handleChange(e) {
    this.setState({chat__message_input: e.target.value});
}

handleSubmit(e) {
    e.preventDefault();

    if (this.state.chat__message_input == "") {
        // create some error here later
        return
    };

    ChatActions.addMessage({
        username: "username",
        message: this.state.chat__message_input,
        message_id: uid("msg-"),
    });  

    // reset input field
    this.setState({chat__message_input: ""});
}
...

Here's the Store

var CHANGE = "change_event";

class ChatStoreClass extends EventEmitter {
    constructor() {
        super();
        this._messages = [
            {
                username: "christian",
                message: "hello world",
                message_id: "msg-12kjds8",
            }
        ];
    }  

    init() {
        this._emitChange();
    }

    getMessages() {
        return this._messages;
    }

    _emitChange() {
        this.emit(CHANGE);
    }


}

var ChatStore = new ChatStoreClass;

AppDispatcher.register(function(payload){
    switch(payload.actionType) {
        case ChatConstants.ADD_MESSAGE:
            ChatStore._messages.push(payload.action);
            break;
    }
})

export default ChatStore;

ChatStore._messages.push(payload.action); is updating the state on the view without a change event being emitted :(

Apologies if poorly explained. I'm not one to usually explain my programming issues to other people let alone anyone.

Thank you for taking the time to read this.


Solution

  • getMessages() is returning a reference to the message list in the store rather than the messages themselves. SetState (e.g. in your handleSubmit) always triggers an update to the component; because you've set this.state.chat__messages to a reference to the store's message list (rather adding its messages to your chat_messages array), it's always up-to-date with the store.

    getMessages() {
        return [...this._messages]; // or w/o ES6 -> return Array.apply([], this._messages)
    }
    

    Should give you the effect you expect.

    I'm sure this was unintended, but part of the confusion is that you're never supposed to mutate state unless through SetState. By using a reference to mutable data, you get an unexpected effect that's pretty tricky to track down.