I'm trying to use Reflux inside my ReactJs app.
I'm having an issue where a component listening to a data store is executing the callback on an empty component instance instead of the actual component that was instanced.
Upon calling the action toggleUserBar, the datastore is called and performs the trigger. In the component Sidebar, the callback is called but the component state is empty:
Uncaught TypeError: Cannot read property 'sd' of undefined
Any idea what I'm doing wrong?
actions.tsx
var Reflux = require('reflux');
var RQ = {};
RQ.actions = Reflux.createActions([
"toggleUserBar"
]);
window.RQ = RQ;
store.tsx
RQ.store = Reflux.createStore({
listenables: [RQ.actions],
init: function() {},
onToggleUserBar: function() {
this.trigger();
}
});
sidebar.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
const LeftNav = require('material-ui/lib/left-nav');
const Menu = require('material-ui/lib/menu/menu')
interface ISidebarProps extends React.Props<any> {
}
interface ISidebarState extends React.Props<any> {
shown: Boolean;
sd: any;
}
export class Sidebar extends React.Component<ISidebarProps, ISidebarState> {
unsubscribe: any;
constructor(props: ISidebarProps) {
super(props);
this.state = { shown: true, sd: null };
};
componentDidMount() {
this.unsubscribe = RQ.store.listen(function() {
this.state.sd.setState({ open: true });
});
};
componentWillUnmount() {
this.unsubscribe();
};
render() {
let menuItems = [
{ route: 'get-started', text: 'Get Started' },
{ route: 'customization', text: 'Customization' },
{ route: 'components', text: 'Components' }
];
return (
<LeftNav ref={(c) => this.state.sd = c} docked={false} openRight={true} menuItems={menuItems} />
);
};
}
The sidebar component is rendered in another component:
<ComSidebar ref={(c) => sdref = c} />
If I manually call sdref.state.sd.open = true, it works just fine.
The this
keyword in JavaScript often behaves in surprising ways. You've found the major one.
When you create an inner function using the function
keyword, it doesn't get the same this
variable as the enclosing function. So that means, in your code:
componentDidMount() {
// "this" out here is your component instance
this.unsubscribe = RQ.store.listen(function() {
// "this" in here is NOT the same - undefined, as you're seeing
this.setState({ open: true });
});
};
There are several ways to fix this. Since you're using ES6, the easiest is probably to use an arrow function - arrow functions explicitly keep the this
reference of their enclosing function. It would look like this:
componentDidMount() {
this.unsubscribe = RQ.store.listen(() => {
// now it's the same "this"
this.setState({ open: true });
});
};
If you weren't using ES6, you could get this same behavior by calling bind
on that function (which allows you to specify the this
variable, as well as any parameters to it):
componentDidMount() {
this.unsubscribe = RQ.store.listen(function() {
this.setState({ open: true });
}.bind(this));
// by calling bind, we are explicitly setting the "this" variable here
// passing it from outer scope to inner scope
};