React Higher Order Component conditional data load

830 views Asked by At

Imagine I have some "page" component, which needs to ask for data from a server. The data it requests will depend on whether or not the current user is authenticated. Further, in the event of a login, the page will want to reload the data. My question is, how can I accomplish something like this using HOCs rather than inheritance?

To illustrate the problem, I'll demonstrate a solution using inheritance. The program will have the following objects. I'll leave out the boilerplate code.

  • session: an EventEmitter that emits start when the session changes (either a login or a log out).
  • Page: the superclass that all pages inherit from
  • MyPage: the subclass of Page in this example
  • API: will be an API class for retrieving data from the server

Here's the code:

// Page superclass
class Page extends React.Component {

   componentWillMount() {
      session.on("start", this.loadData);
      this.loadData();
   }

   loadData() {
      // this method is overwritten in subclasses
   }
}

// MyPage subclass
class MyPage extends Page {

   loadData() {
      if(session.isAuthenticated()) {
         API.loadPrivateData();
      } else {
         API.loadPublicData();
      }
   }
}

Here's a solution that uses an HOC, but seems less elegant than inheritance. It still requires that every "subclass" page have a method loadData, and it requires that method to be called in every "subclass's" componentWillMount.

// Page HOC
function Page(WrappedComponent) {
   return class EnhancedPage extends React.Component {

      componentWillMount() {
         session.on("start", this.loadData);
         // this._page.loadData() will fail here
         // since this._page is undefined until rendering finishes
      }

      loadData() {
         this._page.loadData();
      }

      render() {
         return <WrappedComponent {...props} ref={(e) => { this._page = e; }} />
      }
   }
}

// MyPage
class MyPage extends React.Component {

   componentWillMount() {
      this.loadData();
   }

   loadData() {
      if(session.isAuthenticated()) {
         API.loadPrivateData();
      } else {
         API.loadPublicData();
      }
   }
}

const component = Page(MyPage)

// what would make sense here is to have a method something like
// const component = Page(MyPage, () => MyPage.loadData())
// but then the MyPage.loadData logic would need to be defined
// elsewhere

This pattern will happen often: I'll want to load some data, then reload when the session changes. I'd like to understand the "react" way of accomplishing the same.

EDIT: I am not trying to pass a username or "loggedIn" flag through the HOC. That is to say something like <WrappedComponent isLoggedIn={session.isAuthenticated()} {...props} /> won't cut it here. Tying the API logic to props requires that I check for changes in MyPage.componentWillUpdate().

1

There are 1 answers

0
Koen On BEST ANSWER

When using a HOC you shouldn't place the loadData function on the wrapped component. Instead pass the function as a parameter to the HOC constructor.

Something like this might work for you. The sessionHoc function takes a callback function which'll be called every time the session state changes. Its result will be passed to WrappedComponent as a data prop.

function sessionHoc(onSessionChange) {
    return function (WrappedComponent) {
        return class extends React.Component {
            constructor(props) {
                super(props);

                this.state = {
                    data: null,
                };

                session.on('start', this.handleSessionChange.bind(this));
            }

            handleSessionChange() {
                this.setState({
                    data: onSessionChange(),
                });
            }

            render() {
                return <WrappedComponent data={data} {...this.props} />
            }
        };
    };
}

class MyPage extends React.Component {
    render() {
        // Just access this.props.data here!
    }
}

const EnhancedPage = sessionHoc(function () {
    if (session.isAuthenticated()) {
        return API.loadPrivateData();
    } else {
        return API.loadPublicData();
    }
})(MyPage);

Hopefully this helped! :)