Composition with higher-order components

In this last section of the chapter, we'll cover higher-order components. If you're familiar with higher-order functions in functional programming, higher-order components work the same way. A higher-order function is a function that takes another function as input, and returns a new function as output. This returned function calls the original function in some way. The idea is to compose new behavior out of existing behavior.

With higher-order React components, you have a function that takes a component as input, and returns a new component as output. This is the preferred way to compose new behavior in React applications, and it seems that many of the popular React libraries are moving in this direction if they haven't already. There's simply more flexibility when composing functionality this way.

Conditional component rendering

One obvious use case for a higher-order component is conditional rendering. For example, depending on the outcome of some predicate, the component is rendered or nothing is rendered. The predicate could be anything that's specific to the application, such as permissions or something like that.

Implementing something like this in React is super easy. Let's say we have this super simple component:

import React from 'react'; 
 
// The world's simplest component... 
export default () => ( 
  <p>My component...</p> 
); 

Now, to control the display of this component, we'd wrap it with another component. Wrapping is handled by the higher-order function.

If you hear the term wrapper in the context of React, it's probably referring to a higher-order component. Essentially, this is what it does, it wraps the component you pass to it.

Now let's see how easy it is to create a higher-order React component:

import React from 'react'; 
 
// A minimal higher-order function is all it 
// takes to create a component repeater. Here, we're 
// returning a function that calls "predicate()". 
// If this returns true, then the rendered 
// "<Component>" is returned. 
export default (Component, predicate) => 
  props => 
    predicate() && (<Component {...props} />); 

Only three lines? Are you kidding me? All thanks to the fact that we're returning a functional component. The two arguments to this function are Component, which is the component we're wrapping, and the predicate to call. As you can see, if the call to predicate() returns true, then <Component> is returned. Otherwise, nothing will be rendered.

Now, let's actually compose a new component using this function, and our super simple component that renders a paragraph of text:

import React from 'react'; 
import { render } from 'react-dom'; 
 
import cond from './cond'; 
import MyComponent from './MyComponent'; 
 
// Two compositions of "MyComponent". The 
// "ComposedVisible" version will render 
// because the predicate returns true. The 
// "ComposedHidden" version doesn't render. 
const ComposedVisible = cond(MyComponent, () => true); 
const ComposedHidden = cond(MyComponent, () => false); 
 
render(( 
  <section> 
    <h1>Visible</h1> 
    <ComposedVisible /> 
    <h2>Hidden</h2> 
    <ComposedHidden /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

We've just created two new components using MyComponent, cond(), and a predicate function. That's powerful, if you ask me. Here's the rendered output:

Conditional component rendering

Providing data sources

Let's finish the chapter by looking at a more involved higher-order component example. You'll implement a data store function that wraps a given component with a data source. This type of pattern is handy to know, because it's used by React libraries such as Redux. Here's the connect() function that's used to wrap components:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 
 
// The components that are connected to this store. 
let components = fromJS([]); 
 
// The state store itself, where application data is kept. 
let store = fromJS({}); 
 
// Sets the state of the store, then sets the 
// state of every connected component. 
export function setState(state) { 
  store = state; 
 
  for (const component of components) { 
    component.setState({ 
      data: store, 
    }); 
  } 
} 
 
// Returns the state of the store. 
export function getState() { 
  return store; 
} 
 
// Returns a higher-order component that's connected 
// to the "store". 
export function connect(ComposedComponent) { 
  return class ConnectedComponent extends Component { 
 
    state = { data: store } 
 
    // When the component is mounted, add it to 
    // "components", so that it will receive updates  
    // when the store state changes. 
    componentWillMount() { 
      components = components.push(this); 
    } 
 
    // Deletes this component from "components" when it is 
    // unmounted from the DOM. 
    componentWillUnmount() { 
      const index = components.findIndex(this); 
      components = components.delete(index); 
    } 
 
    // Renders "ComposedComponent", using the "store" state 
    // as properties. 
    render() { 
      return (<ComposedComponent {...this.state.data.toJS()} />); 
    } 
  }; 
} 

This module defines two internal immutable objects: components and store. The components list holds references to components that are listening to store changes. The store represents the application state as a whole.

The concept of a store stems from Flux, a set of architectural patterns used to build large-scale React applications. We'll touch on Flux ideas here and there throughout this book, but as a whole, Flux goes way beyond the scope of this book.

The important pieces of this module are the exported functions: setState(), getState(), and connect(). The getState() function simply returns a reference to the data store. The setState() function sets the state of the store, then notifies all components that the state of the application has changed. The connect() function is the higher-order function that wraps the given component with a new one. When the component is mounted, it registers itself with the store so that it will receive updates when the store changes state. It renders the composed component by passing the store as properties.

Now let's use this utility to build a simple filter and list. First, the list component:

import React, { PropTypes } from 'react'; 
 
// Renders an item list... 
const MyList = ({ items }) => ( 
  <ul> 
    {items.map(i => ( 
      <li key={i}>{i}</li> 
    ))} 
  </ul> 
); 
 
MyList.propTypes = { 
  items: PropTypes.array.isRequired, 
}; 
 
export default MyList; 

Not too much happening here that you haven't already seen. Now let's look at the filter component:

import React, { PropTypes } from 'react'; 
import { fromJS } from 'immutable'; 
import { getState, setState } from './store'; 
 
// When the filter input value changes. 
function onChange(e) { 
  // The state that we're working with... 
  const state = getState(); 
  const items = state.get('items'); 
  const tempItems = state.get('tempItems'); 
 
  // The new state that we're going to set on 
  // the store. 
  let newItems; 
  let newTempItems; 
 
  // If the input value is empty, we need to restore the 
  // items from "tempItems". 
  if (e.target.value.length === 0) { 
    newItems = tempItems; 
    newTempItems = fromJS([]); 
  } else { 
    // If "tempItems" hasn't been set, make sure that 
    // it gets the current items so that we can restore 
    // them later. 
    if (tempItems.isEmpty()) { 
      newTempItems = items; 
    } else { 
      newTempItems = tempItems; 
    } 
 
    // Filter and set "newItems". 
    const filter = new RegExp(e.target.value, 'i'); 
    newItems = items.filter(i => filter.test(i)); 
  } 
 
  // Updates the state of the store. 
  setState(state.merge({ 
    items: newItems, 
    tempItems: newTempItems, 
  })); 
} 
 
// Renders a simple input element to filter a list. 
const MyInput = ({ value, placeholder }) => ( 
  <input 
    autoFocus 
    value={value} 
    placeholder={placeholder} 
    onChange={onChange} 
  /> 
); 
 
MyInput.propTypes = { 
  value: PropTypes.string, 
  placeholder: PropTypes.string, 
}; 
 
export default MyInput; 

The MyInput component itself is quite simple; it's just an <input> element. It's the onChange handler that needs some explanation, so let's spend some time here. The goal of this handler is to filter the user list so that only items that contain the current input text are displayed. Since the MyList component doesn't actually filter anything that's passed to it, this handler needs to alter what's passed to it. This is the essence of having a centralized store that holds the application state—it's how different components communicate.

The way we handle filtering the user list involves making a copy of it before anything else happens. This is because we need to actually modify the items state. Since we have the original, this is easy to restore later. Once the filtering has been performed, we use setState() to let the other components know that the application has changed state.

Here's what the rendered filter input and item list looks like:

Providing data sources

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

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