Search code examples
javascriptreactjsrefluxjs

ReactJs + Reflux // trigger is executed on null component?


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.


Solution

  • 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
    };