Cloning components

"Shawn, props are also immutable in React. In most of the cases, the child component just uses props passed by the parent component. However, sometimes we want to extend the incoming props with some new data before rendering the child component. It's a typical use case to update styles and CSS classes. React provides an addon to clone a component and extending its props. It's called the cloneWithProps addon." said Mike.

"Mike, that addon is deprecated. I had looked at it in the past and React has deprecated it. It also has lot of issues related to refs of the child component not getting passed to the newly-cloned child components," Shawn informed.

"True. However, React also has a top-level React.cloneElement API method, which allows us to clone and extend a component. It has a very simple API and it can be used instead of the cloneWithProps addon." Mike explained.

React.cloneElement(element, props, …children);

"This method can be used to clone the given element and merge the new props with existing props. It replaces existing children with new children. Therefore, we have to keep this in mind while dealing with the child components."

"The cloneElement function also passes ref of the old child component to the newly-cloned component. Therefore, if we have any callbacks based on refs, they will continue to work even after cloning."

"Shawn, here is your next challenge. We show listing of books in our app. In fact, in all of our apps, we show the listing of other things such as products, items, and so on. We want to show the rows with alternate colors in all of these listings instead of just white background. As the code for this feature will be the same across all the apps, I am thinking about creating a separate component that will accept rows as input and render them with alternate colors. Such components can be used in all of our apps. I think that you can make use of React.cloneElement for this." Mike explained the next task.

"Sounds like a good idea to extract this as a separate component. We need it in almost all the apps. Our QA was complaining about the lack of colors in our search app yesterday." Shawn remembered.

"Let's add some alternate colors then." Mike chuckled.

"First, let's see how we are displaying books currently."

// src/App.js

render() {
    let tabStyles = {paddingTop: '5%'};
    return (
      <div className='container'>
        <div className="row" style={tabStyles}>
          <div className="col-lg-8 col-lg-offset-2">
            <h4>Open Library | Search any book you want!</h4>
            <div className="input-group">
              <input type="text" className="form-control" placeholder="Search books..." ref='searchInput'/>
              <span className="input-group-btn">
                <button className="btn btn-default" type="button" onClick={this._performSearch}>Go!</button>
              </span>
            </div>
          </div>
        </div>
        {this._displaySearchResults()}
      </div>
    );
  },

_displaySearchResults() {
    if(this.state.searching) {
      return <Spinner />;
    } else if(this.state.searchCompleted) {
      return (
        <BookList
            searchCount={this.state.totalBooks}
            _sortByTitle={this._sortByTitle}>
          {this._renderBooks()}
        </BookList>
      );
    }
  } 

_renderBooks() {
    return this.state.books.map((book, idx) => {
      return (
        <BookRow key={idx}
                 title={book.title}
                 author_name={book.author_name}
                 edition_count={book.edition_count} />
      );
    })
  },

})
  }

"The BookList component just renders the rows passed to it as it is using this.props.children."

// BookList component

var BookList = React.createClass({
  render() {
    return (
      <div className="row">
        <div className="col-lg-8 col-lg-offset-2">
          <span className='text-center'>
            Total Results: {this.props.searchCount}
          </span>
          <table className="table table-stripped">
            <thead>
              <tr>
                <th><a href="#" onClick={this.props._sortByTitle}>Title</a></th>
                <th>Author</th>
                <th>No. of Editions</th>
              </tr>
            </thead>
            <tbody>
              {this.props.children}
            </tbody>
          </table>
        </div>
      </div>
    );
  }
});

"Mike, I am naming the component RowAlternator. The RowAlternator component will get the dynamic array of children rows and it will render them with alternate colors. We can pass multiple colors to RowAlternator too. In this way, the client code using this component can control the colors that they want to use as alternate colors."

"Sounds good, Shawn. I think this much API is enough for now."

// RowAlternator component

import React from 'react';

var RowAlternator = React.createClass({
  propTypes: {
    firstColor: React.PropTypes.string,
    secondColor: React.PropTypes.string
  },

  render() {
    return (
      <tbody>
        { this.props.children.map((row, idx) => {
            if (idx %2 == 0) {
              return React.cloneElement(row, { style: { background: this.props.firstColor }});
            } else {
              return React.cloneElement(row, { style: { background: this.props.secondColor }});
            }
          })
        }
      </tbody>
    )
  }
});

module.exports = RowAlternator;

"As we don't know how many children elements we will get in RowAlternator, we will just iterate over all of them and set style with alternate colors. We are also using React.cloneElement here to clone the passed child and extend its style prop with appropriate background color."

"Let's change our BookList component now in order to use RowAlternator."

// BookList component

import RowAlternator from '../src/RowAlternator';

var BookList = React.createClass({
  render() {
    return (
      <div className="row">
        <div className="col-lg-8 col-lg-offset-2">
          <span className='text-center'>
            Total Results: {this.props.searchCount}
          </span>
          <table className="table table-stripped">
            <thead>
              <tr>
                <th><a href="#" onClick={this.props._sortByTitle}>Title</a></th>
                <th>Author</th>
                <th>No. of Editions</th>
              </tr>
            </thead>
            <RowAlternator firstColor="white" secondColor="lightgrey">
              {this.props.children}
            </RowAlternator>
          </table>
        </div>
      </div>
    );
  }
});

"We are all set. The listing now shows alternate colors as we wanted, as shown in the following image:"

Cloning components

"Perfect, Shawn. As you already noticed, using React.cloneElement makes sense when we are building a component with dynamic children, where we don't have control on the render method of these children, but want to extend their props based on some criteria." Mike was happy.

Helpers for testing React apps

"Shawn, we have not added any tests for our app yet, however, the time has come to start slowly adding test coverage. With the Jest library and Test Utilities addon, it becomes very easy to set up and start testing the React apps." Mark explained the next task.

"I have heard about Jest. Isn't it a testing library by Facebook?" Shawn asked.

Setting up Jest

"Yes. It's built on top of Jasmine. It's very easy to set up. First, we have to install the jest-cli and babel-jest packages."

npm install jest-cli --save-dev
npm install babel-jest –-save-dev

"After that, we need to configure package.json, as follows:"

{
 ...
 "scripts": {
   "test": "jest"
 },

 "jest": {
   "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
    "unmockedModulePathPatterns": [
         "<rootDir>/node_modules/react",
         "<rootDir>/node_modules/react-dom",
         "<rootDir>/node_modules/react-addons-test-utils",
         "<rootDir>/node_modules/fbjs"
     ],
   "testFileExtensions": ["es6", "js", "jsx"],
   "moduleFileExtensions": ["js", "json", "es6"]
 }
 ...
}

"By default, Jest mocks all modules, however, here we are telling Jest not to mock React and the related libraries. We are also specifying the extensions for our test file that will be identified by Jest as test files."

"Create a __test__ folder, where we will be adding our tests. Jest will run the tests from files in this folder. Let's add an empty file too. We have to make sure that the file should end with -test.js so that Jest will pick up it to run the tests." Mike explained.

mkdir __test__
touch __test__/app-test.js

"Now let's verify that we can run the tests from the command line."

$ npm test

> [email protected] test /Users/prathamesh/Projects/sources/reactjs-by-example/chapter7
> jest

Using Jest CLI v0.7.1
PASS __tests__/app-test.js (0.007s)

Note

You should see an output that is similar to the preceding output. It may change based on the Jest version. Consult https://facebook.github.io/jest/docs/getting-started.html to set up Jest, in case of any issues.

"Shawn, we are all set with Jest. It's time to start writing the tests now. We will test whether the top-level App component gets mounted correctly. However, first, we need to understand a bit more about using Jest," said Mike.

"By default, Jest mocks all the modules that are required in a test file. Jest does this to isolate the module that is under test from all the other modules. In this case, we want to test the App component. If we just require it, then Jest will provide a mocked version of App."

// app-test.js
   
   const App = require('App'); // Mocked by Jest

"As we we want to test the App component itself, we need to specify Jest not to mock it. Jest provides the jest.dontmock() function for this purpose."

// app-test.js

jest.dontmock('./../src/App'); // Tells Jest not to mock App.
const App = require('App');

Note

Check https://facebook.github.io/jest/docs/automatic-mocking.html for more details about automatic mocking feature of Jest.

"Next up, we will add imports for the React and TestUtils addon."

// app-test.js

jest.dontMock('../src/App');
const App = require('../src/App');

import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';

"The TestUtils addon provides utility functions to render the components, finding sub-components in rendered components and mimicking events on the rendered component. It helps in testing both the structure and behavior of the React components." Mike added.

Testing structure of React components

"We will start with the renderIntoDocument function. This function renders the given component into a detached DOM node in the document. It returns ReactComponent, which can be used for further testing."

// app-test.js

describe('App', () => {
  it('mounts successfully', () => {
    let app = TestUtils.renderIntoDocument(<App />);
    expect(app.state.books).toEqual([]);
    expect(app.state.searching).toEqual(false);
  })
});

"We rendered the App component in DOM and asserted that initially books and searching state are being set correctly." Mike explained.

"Mike, this is awesome. We are not testing real DOM, but testing the React components instead."

"Yes. The TestUtils addon comes with finder methods that can be used to find the child components in a given component tree. They are useful to find the child components such as input box and submit button and simulate click events or change events."

  • findAllInRenderedTree(tree, predicate function): This is useful for finding all the components in a given tree that returns the truth value for the predicate function.
  • scryRenderedDOMComponentsWithClass(tree, className): This is useful for finding all the DOM components with a given class name.
  • findRenderedDOMComponentWithClass(tree, className): Instead of finding all the DOM components with a given class, this method expects that only one such component is present. It throws an exception if there are multiple components with a given class name.
  • scryRenderedDOMComponentsWithTag(tree, tagName): It's similar to finding the DOM components with the class name, however, instead of class name, it finds the components based on a given tag name.
  • findRenderedDOMComponentWithTag(tree, tagName): Instead of finding all the components with a given tag, it expects that only one such component is present. It also throws exception when more than one such components exists.
  • scryRenderedComponentsWithType(tree, componentType): This method finds all the components with a given type. It's useful to find all the composite components created by the user.
  • findRenderedComponentWithType (tree, componentType): This is similar to all the previous finder methods. It raises exception if more than one component with a given type is present.

Testing behavior of React components

"Let's use these functions to assert that the search for books starts when a user enters a search term and clicks the Submit button. We will simulate the event of entering search term by the user." said Mike.

// app-test.js

it('starts searching when user enters search term and clicks submit', () => {
    let app = TestUtils.renderIntoDocument(<App />);

    let inputNode = TestUtils.findRenderedDOMComponentWithTag(app, 'input');
    inputNode.value = "Dan Brown";
    TestUtils.Simulate.change(inputNode);
    let submitButton = TestUtils.findRenderedDOMComponentWithTag(app, 'button');
    TestUtils.Simulate.click(submitButton);
    expect(app.state.searching).toEqual(true);
    expect(app.state.searchCompleted).toEqual(false);
  })

"We render the App component, find the input node, set the value of the input node to the search term, and simulate the change event using TestUtils.Simulate(). This function simulates the given event dispatch on a DOM node. The Simulate function has a method for every event that React understands. Therefore, we can simulate all events such as change, click, and so on. We can test the user behavior using this method," Mike explained.

"Got it. Therefore, after changing the search term, we click the submit button and verify that the state gets updated as per our expectations," informed Shawn.

"Yes, Shawn. Now, can you check whether the Spinner is shown once the user clicks the Submit button? You can use one of the finder method that we discussed earlier." Mike explained the next task.

"Yes. Once the component state changes after clicking the Submit button, we can search the rendered component tree to see whether the Spinner component is present or not."

// app-test.js

// __tests__/app-test.js

import Spinner from './../src/Spinner';

it('starts searching when user enters search term and clicks submit', () => {
    let app = TestUtils.renderIntoDocument(<App />);
    let inputNode = TestUtils.findRenderedDOMComponentWithTag(app, 'input');
    inputNode.value = "Dan Brown";
    TestUtils.Simulate.change(inputNode);
    let submitButton = TestUtils.findRenderedDOMComponentWithTag(app, 'button');
    TestUtils.Simulate.click(submitButton);
    expect(app.state.searching).toEqual(true);
    expect(app.state.searchCompleted).toEqual(false);
    let spinner = TestUtils.findRenderedComponentWithType(app, Spinner);
    expect(spinner).toBeTruthy();
  }),

"We are using TestUtils.findRenderedComponentWithType here to check whether Spinner is present in the tree rendered by the App component or not. However, before adding this assertion, we need to import the Spinner component at the top of the test file as findRenderedComponentWithType expects a second argument to be a React component."

"Excellent, Shawn. As you can see, the testing behavior of the React component becomes very easy with the TestUtils.Simulate and finder methods." Mike explained.

Note

Note that we have not added a test to asynchronously load the books from Open Library API as it is out of the scope of this chapter.

Shallow rendering

"Shawn, TestUtils also provides one more way to test the components in an isolated fashion using the Shallow rendering. Shallow rendering allows us render the top-level component and assert the facts about the return value of it's render method. It does not render children components or instantiate them. Therefore, our tests are not affected by the behavior of the children components. Shallow rendering also does not require a DOM, unlike the previous methods, where we rendered the component in a detached DOM," Mike explained.

let renderer = TestUtils.createRenderer();

"This creates a shallow renderer object, in which we will render the component that we want to test. Shallow renderer has a render method similar to ReactDOM.render, which can be used to render the component."

let renderer = TestUtils.createRenderer();
let result = renderer.render(<App />);

"After render method is called, we should call renderer.getRenderedOutput, which returns the shallowly rendered output of rendering the component. We can start asserting facts about the component on the output of getRenderedOutput."

"Let's see the output we get from getRenderedOutput."

let renderer = TestUtils.createRenderer();
let result = renderer.render(<App />);
result = renderer.getRenderOutput();
console.log(result);

// Output of console.log(result)

Object {
  '$$typeof': Symbol(react.element),
  type: 'div',
  key: null,
  ref: null,
  props: 
   Object {
     className: 'container',
     children: Array [ [Object], undefined ] },
  _owner: null,
  _store: Object {} }

"As you can see, based on the rendered output, we can assert the facts about props of the current component. However, if we want to test anything about children component, we need to explicitly reach out to them through this.props.children[0].props.children[1].props.children."

"This makes it hard to test the behavior of the children components using shallow rendering. However, it's useful for testing small components in an isolated way as it does not get affected by children component due to shallow rendering," said Mike.

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

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