Chapter 8. Extending Components

In this chapter, you'll learn how to add new capabilities to existing components by extending them. There are two React mechanisms that you can use to extend a component, and we'll look at each in turn:

  • Component inheritance
  • Composition with higher-order components

We'll start by looking at basic component inheritance, just like the good old object-oriented class hierarchies that you're used to. Then we'll implement some higher-order components used as the ingredients in React component composition.

Component inheritance

Components are just classes. In fact, when you implement a component using ES2015 class syntax, you extend the base Component class from React. You can keep on extending your classes like this to create your own base components.

In this section, you'll see how your components can inherit state, properties, and just about anything else, including JSX markup and event handlers.

Inheriting state

Sometimes, you have several React components that use the same initial state. You can implement a base component that sets this initial state. Then, any components that want to use this as their initial state can extend this component. Let's implement a base component that sets some basic state:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 
 
export default class BaseComponent extends Component { 
  state = { 
    data: fromJS({ 
      name: 'Mark', 
      enabled: false, 
      placeholder: '', 
    }), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  // The base component doesn't actually render anything, 
  // but it still needs a render method. 
  render() { 
    return null; 
  } 
} 

As you can see, the state is an immutable Map. This base component also implements the immutable data setter and getter methods. Let's implement a component that extends this one:

import React from 'react'; 
import BaseComponent from './BaseComponent'; 
 
// Extends "BaseComponent" to inherit the 
// initial component state. 
export default class MyComponent extends BaseComponent { 
 
  // This is our chance to build on the initial state. 
  // We change the "placeholder" text and mark it as 
  // "enabled". 
  componentWillMount() { 
    this.data = this.data 
      .merge({ 
        placeholder: 'Enter a name...', 
        enabled: true, 
      }); 
  } 
 
  // Renders a simple input element, that uses the 
  // state of this component as properties. 
  render() { 
    const { 
      enabled, 
      name, 
      placeholder, 
    } = this.data.toJS(); 
 
    return ( 
      <label htmlFor="my-input"> 
        Name: 
        <input 
          id="my-input" 
          disabled={!enabled} 
          defaultValue={name} 
          placeholder={placeholder} 
        /> 
      </label> 
    ); 
  } 
} 

This component doesn't actually have to set any initial state because it's already set by BaseComponent. Since the state is already an immutable Map, we can tweak the initial state in componentWillMount() using merge(). Here's what the rendered output looks like:

Inheriting state

And if you delete the default text in the input element, you can see that the placeholder text added by MyComponent to the initial state is applied as expected:

Inheriting state

Inheriting properties

Inheriting properties works exactly how you would expect. You define the default property values and the property types as static properties in a base class. Any classes that inherit this base also inherit the property values and the property specs. Let's take a look at a base class implementation:

import React, { Component, PropTypes } from 'react'; 
 
export default class BaseComponent extends Component { 
  // The specification for these base properties. 
  static propTypes = { 
    users: PropTypes.array.isRequired, 
    groups: PropTypes.array.isRequired, 
  } 
 
  // The default values of these base properties. 
  static defaultProps = { 
    users: [], 
    groups: [], 
  } 
 
  render() { 
    return null; 
  } 
} 

The class itself doesn't actually do anything. The only reason we're defining it is so that there's a place to declare the default property values and their type constraints. Respectively, these are the defaultProps and the propTypes static class attributes.

Now, let's take a look at a component that inherits these properties:

import React from 'react'; 
import { Map as ImmutableMap } from 'immutable'; 
 
import BaseComponent from './BaseComponent'; 
 
// Renders the given "text" as a header, unless 
// the given "length" is 0. 
const SectionHeader = ({ text, length }) => 
  ImmutableMap() 
    .set(0, null) 
    .get(length, (<h1>{text}</h1>)); 
 
export default class MyComponent extends BaseComponent { 
  render() { 
    const { users, groups } = this.props; 
 
    // Renders the "users" and "groups" arrays. There 
    // are not property validators or default values 
    // in this component, since these are declared in 
    // "BaseComponent". 
    return ( 
      <section> 
        <SectionHeader 
          text="Users" 
          length={users.length} 
        /> 
        <ul> 
          {users.map(i => ( 
            <li key={i}>{i}</li> 
          ))} 
        </ul> 
 
        <SectionHeader 
          text="Groups" 
          length={groups.length} 
        /> 
        <ul> 
          {groups.map(i => ( 
            <li key={i}>{i}</li> 
          ))} 
        </ul> 
      </section> 
    ); 
  } 
} 

Let's try rendering MyComponent to make sure that the inherited properties are working as expected:

import React from 'react'; 
import { render } from 'react-dom'; 
 
import MyComponent from './MyComponent'; 
 
const users = [ 
  'User 1', 
  'User 2', 
]; 
 
const groups = [ 
  'Group 1', 
  'Group 2', 
]; 
 
render(( 
  <section> 
    { /* Renders as expected, using the defaults. */ } 
    <MyComponent /> 
 
    { /* Renders as expected, using the "groups" default. */ } 
    <MyComponent users={users} /> 
    <hr /> 
 
    { /* Renders as expected, using the "users" default. */ } 
    <MyComponent groups={groups} /> 
    <hr /> 
 
    { /* Renders as expected, providing property values. */ } 
    <MyComponent users={users} groups={groups} /> 
 
    { /* Fails to render, the property validators in the base 
         component detect the invalid number type. */ } 
    <MyComponent users={0} groups={0} /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

As you can see, despite the fact that MyComponent doesn't define any property defaults or types, we get the behavior we'd expect. When we try to pass numbers to the users and groups properties, we don't see anything rendered. That's because MyComponent is expecting a map() method on these property values, and there isn't one. However, before this exception happens, you can see the property validation failure warning, which explains exactly what happened. In this case, we passed an unexpected type.

If we were to remove this last element, everything else renders fine. Here's what the rendered content looks like:

Inheriting properties

Inheriting JSX and event handlers

The last area we'll touch upon with React component inheritance is JSX and event handlers. You might want to take this approach if you have a single UI component that has the same UI elements and event handling logic, but there are differences in what the initial state should be, depending on where the component is used.

For example, a base class would define the JSX and event handler methods while the more specific components define the initial state that's unique to the feature. Here's an example base class:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 
 
export default class BaseComponent extends Component { 
  state = { 
    data: fromJS({ 
      items: [], 
    }), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  // The click event handler for each item in the 
  // list. The context is the lexically-bound to 
  // this component. 
  onClick = id => () => { 
    this.data = this.data 
      .update( 
        'items', 
        items => items 
          .update( 
            items.indexOf(items.find(i => i.get('id') === id)), 
            item => item.update('done', d => !d) 
          ) 
      ); 
  }; 
 
  // Renders a list of items based on the state 
  // of the component. The style of the item 
  // depends on the "done" property of the item. 
  // Each item is assigned an event handler that 
  // toggles the "done" state. 
  render() { 
    const { items } = this.data.toJS(); 
 
    return ( 
      <ul> 
        {items.map(i => ( 
          <li 
            key={i.id} 
            onClick={this.onClick(i.id)} 
            style={{ 
              cursor: 'pointer', 
              textDecoration: i.done ? 
                'line-through' : 'none', 
            }} 
          >{i.name}</li> 
        ))} 
      </ul> 
    ); 
  } 
} 

This base component renders a list of items that, when clicked, toggles the style of the item text. By default, the state of this component has an empty item list. This means that it is safe to render this component without setting the component state. However, that's not very useful, so let's give this list some items by inheriting the base component and setting the state:

import React from 'react'; 
import { fromJS } from 'immutable'; 
import BaseComponent from './BaseComponent'; 
 
export default class MyComponent extends BaseComponent { 
 
  // Initializes the component state, by using the 
  // "data" getter method from "BaseComponent". 
  componentWillMount() { 
    this.data = this.data 
      .merge({ 
        items: [ 
          { id: 1, name: 'One', done: false }, 
          { id: 2, name: 'Two', done: false }, 
          { id: 3, name: 'Three', done: false }, 
        ], 
      }); 
  } 
} 

Remember, the componentWillMount() lifecycle method can safely set the state of the component. Here, the base component uses our data setter/getter to change the state of the component. Another thing that's handy about this approach is that if we want to override one of the event handlers of the base component, that's easy to do: simply define the method in MyComponent.

Here's what the list looks like when rendered:

Inheriting JSX and event handlers

And here's what the list looks like when all of the items have been clicked:

Inheriting JSX and event handlers

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

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