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.
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.