Higher order components and subscriptions which depend on state


(Simon Williams) #1

We are currently refactoring to use higher-order components. For the most part this is making everything much simpler.

We have HOCs for fetching data and listening to stores. For example, we have connectStores, which takes a list of stores to subscribe to and a function to fetch the data (to pass as extra props):

connectStores(FooComponent, [FooStore], function (props) {
    return {
        foo: FooStore.get(props.id),
    };
});

However, there are a few places where the process of fetching the data from the store depends upon the state. For example, we have a SelectFooPopup the presents the user with a list of items to select from. But there is also a search box to filter the list, so at the moment the component listens directly to the store and then fetches the data itself like this:

componentDidMount() {
    var self = this;
    this.listenTo(FooStore, 'change', function () {
        self.forceUpdate();
    });
}

render() {
    var items = FooStore.search(this.state.searchText);
    // render...
}

(this.listenTo is a mixin which we’re trying to replace with HOCs so we can use ES6 classes)

I can think of a few options, but I don’t like any of them:

Option 1: Remove listenTo and cleanup the listener manually

componentDidMount() {
    var self = this;
    this.listener = function () {
        self.forceUpdate();
    };

    FooStore.on('change', this.listener);
}

componentWillUnmount() {
    if (this.listener) {
        FooStore.removeListener('change', this.listener);
    }
}

render() {
    var items = FooStore.search(this.state.searchText);
    // render...
}

I really hate having to do this manually. We did this before we had the listenTo mixin and it’s far too easy to get wrong.

Option 2: Use connectStores but don’t return any extra data

class SelectFooPopup extends React.Component {
    render() {
        var items = FooStore.search(this.state.searchText);
    }
}

connectStores(SelectFooPopup, [FooStore], function (props) {
    // Just to forceUpdate
    return {};
});

This just feels wrong to me. This is asking for trouble when we start optimising for pure components and suddenly the child component doesn’t re-render anymore.

Option 3: Use connectStores to fetch all the data and then filter it in render

class SelectFooPopup extends React.Component {
    render() {
        var items = filterSearch(this.props.items, this.state.searchText);
    }
}

connectStores(SelectFooPopup, [FooStore], function (props) {
    return {
        items: FooStore.getAllItems(),
    };
});

But now I have to have a completely separate filterSearch function. Shouldn’t this be a method on the store?

Also, it doesn’t make much difference in this example, but I have other components with a similar issue where
they are fetching data from the server rather than subscribing to a pre-filled store. In these cases the
data set is far too large to send it all and filter later, so the searchText must be available when fetching the data.

Option 4: Create a parent component to hold the state

Sometimes this is the right solution. But it doesn’t feel right here. The searchText is part of the state of this component. It belongs in the same place that renders the search box.

Moving it to a separate component is confusing and artificial.

Option 5: Use a “parentState” HOC

function parentState(Component, getInitialState) {
    class ParentStateContainer extends React.Component {
        constructor(props) {
            super();
            this.setParentState = this.setParentState.bind(this);
            if (getInitialState) {
                this.state = getInitialState(props);
            } else {
                this.state = {};
            }
        }

        setParentState(newState) {
            this.setState(newState);
        }

        render() {
            return <Component {...this.props} {...this.state} setParentState={ this.setParentState } />;
        }
    }
    return ParentStateContainer;
}

// Usage:
parentState(SelectFooPopup, function (props) {
    return {
        searchText: '',
    };
});

// In handleSearchText:
this.props.setParentState({ searchText: newValue });

This also feels really wrong and I should probably throw this away.

Conclusion

In React we have 2 levels: props and state.

It seems to me that there are actually 4 levels to think about:

  1. props
  2. data that depends on props only
  3. state
  4. data that depends on props and state
  5. render

We can implement layer 2 using HOCs. But how can we implement layer 4?

Any thoughts?

Thanks.