Component state

Until now, we've dealt with React as a stateless rendering engine, but as we know, applications have state, especially when using forms. So, how would we implement the NewInvestment component in order for it to keep hold of the values of the investment being created and then notify an observer once the user completed the form?

To help us implement this behavior, we are going to use another component internal API—its state.

Let's take the following acceptance criterion:

Given that the inputs of the NewInvestment component are correctly filled, when the form is submitted, it should notify the onCreate observer with the investment attributes:

describe("NewInvestment", function() {
  var TestUtils = React.addons.TestUtils;
  var component, onCreateSpy;

  function findNodeWithClass (className) {
    return TestUtils.findRenderedDOMComponentWithClass(component, className).getDOMNode();
  }

  beforeEach(function() {
    onCreateSpy = jasmine.createSpy('onCreateSpy'),
    component = TestUtils.renderIntoDocument(
      <NewInvestment onCreate={onCreateSpy}/>
    );
  });

  describe("with its inputs correctly filled", function() {
    beforeEach(function() {
      var stockSymbol = findNodeWithClass('new-investment-stock-symbol'),
      var shares = findNodeWithClass('new-investment-shares'),
      var sharePrice = findNodeWithClass('new-investment-share-price'),

      TestUtils.Simulate.change(stockSymbol, { target: { value: 'AOUE' }});
      TestUtils.Simulate.change(shares, { target: { value: '100' }});
      TestUtils.Simulate.change(sharePrice, { target: { value: '20' }});
    });

    describe("when its form is submitted", function() {
      beforeEach(function() {
        var form = component.getDOMNode();
        TestUtils.Simulate.submit(form);
      });

      it("should invoke the 'onCreate' callback with the investment attributes", function() {
        var investmentAttributes = { stockSymbol: 'AOUE', shares: '100', sharePrice: '20' };

        expect(onCreateSpy).toHaveBeenCalledWith(investmentAttributes);
      });
    });
  });
});

This spec is basically using every trick we've learned until now, so without getting into the details, let's dive directly into the component implementation.

The first thing that any component with state must declare is its initial state by defining a getInitialState method, as follows:

var NewInvestment = React.createClass({
   getInitialState: function () {
    return {
      stockSymbol: '',
      shares: 0,
      sharePrice: 0
    };
  
},

  render: function () {
    var state = this.state;

    return <form className="new-investment">
      <h1>New investment</h1>
      <label>
        Symbol:
        <input type="text" ref="stockSymbol" className="new-investment-stock-symbol" value={state.stockSymbol} maxLength="4"/>
      </label>
      <label>
        Shares:
        <input type="number" className="new-investment-shares" value={state.shares}/>
      </label>
      <label>
        Share price:
        <input type="number" className="new-investment-share-price" value={state.sharePrice}/>
      </label>
      <input type="submit" className="new-investment-submit" value="Add"/>
    </form>;
  }
});

As illustrated in the preceding code, we are clearly defining the initial state of our form, and at the render method, we pass the state as value props to the input components.

If you run this example in a browser, you will notice that you won't be able to change the values of the inputs. You can focus on the inputs, but trying to type won't change its values, and that is because of the way React works.

Unlike HTML, React components must represent the state of the view at any point in time and not only at initialization time. If we want to change the value of an input, we need to listen for the onChange events of the inputs and, with that information, update the state. The change in the state will then trigger a render that will update the value on screen.

To demonstrate how this works, let's implement this behavior at the stockSymbol input.

First, we need to change the render method, adding a handler to the onChange event:

<input type="text" ref="stockSymbol" className="new-investment-stock-symbol" value={state.stockSymbol} maxLength="4" onChange={this._handleStockSymbolChange}/>

Once the event is triggered, it will invoke the _handleStockSymbolChange method. Its implementation should update the state by invoking the this.setState method with the new value of the input, as follows:

var NewInvestment = React.createClass({
  getInitialState: function () {
    // ... Method implementation
  },

  render: function () {
    // ... Method implementation
  },

  
_handleStockSymbolChange: function (event) {
    this.setState({ stockSymbol: event.target.value });
  }
});

The event handler is a good place to perform simple validation or transformation in the input data before passing it to the state.

As you can see, this is a lot of boilerplate code just to handle a single input. Since we are not implementing any custom behavior into our event handlers, we can use a special React feature that implements this "linked state" for us.

We are going to use a Mixin called LinkedStateMixin; but first, what is a Mixin? It is a way to share common functionality between components, which, in this case, is the "linked state". Take a look at the following code:

var NewInvestment = React.createClass({
  
mixins: [React.addons.LinkedStateMixin],

  // ...

  render: function () {
    // ...
    <input type="text" ref="stockSymbol" className="new-investment-stock-symbol" valueLink={this.linkState('stockSymbol')} maxLength="4" />
    // ...
  }
});

LinkedStateMixin works by adding the linkState function to the component, and instead of setting the value of the input, we set a special prop called valueLink with the link object returned by the function this.linkState.

The linkState function expects the name of the attribute of the state that it should link to the value of the input.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset