anivas
anivas

Reputation: 6547

React - Call wrapped component callback from higher order function

I have a higher order function that wraps the service calls. The data is streamed on a callback which I have to pass to the wrapped components. I have written the code below currently, where the child assigns handleChange to an empty object passed by the HOC. The wrapped component is a regular JS grid and hence I have to call the api to add data than pass it as a prop. Is there a cleaner way of doing this?

    function withSubscription(WrappedComponent) {     
    return class extends React.Component {
      constructor(props) {
        super(props);       
        this.handler = {};
      }

      componentDidMount() { 
        DataSource.addChangeListener(this.handleChange);
      }

      componentWillUnmount() {
        DataSource.removeChangeListener(this.handleChange);
      }

      handleChange(row) {
        if (typeof this.handler.handleChange === "function") {
            this.handler.handleChange(row);
        }
      }

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

  class MyGrid extends React.Component {
     constructor(props) {
         super(props);
         if (props.serviceHandler !== undefined) {
             props.serviceHandler.handleChange = this.handleChange.bind(this);
         }

         this.onReady = this.onReady.bind(this);
     }

     onReady(evt) {
         this.gridApi = evt.api;
     }

     handleChange(row) {
        this.gridApi.addRow(row);
     }

     render() { 
        return <NonReactGrid onReady={this.onReady} />;
      }

  }

  const GridWithSubscription = withSubscription(MyGrid);

Upvotes: 1

Views: 1853

Answers (1)

Estus Flask
Estus Flask

Reputation: 222493

That wrapped component should be aware of handler.handleChange looks awkward.

If withSubscription can be limited to work with stateful components only, a component may expose changeHandler hook:

function withSubscription(WrappedComponent) {
  return class extends React.Component {
    ...
    wrappedRef = React.createRef();

    handleChange = (row) => {
      if (typeof this.wrappedRef.current.changeHandler === "function") {
        this.wrappedRef.current.changeHandler(row);
      }
    }

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

class MyGrid extends React.Component {
  changeHandler = (row) => {
    ...
  }
}

const GridWithSubscription = withSubscription(MyGrid);

To work with stateful and stateless components withSubscription should be made more generalized to interact with wrapped component via props, i.e. register a callback:

function withSubscription(WrappedComponent) {
  return class extends React.Component {
    handleChange = (row) => {
      if (typeof this.changeHandler === "function") {
        this.changeHandler(row);
      }
    }

    registerChangeHandler = (cb) => {
      this.changeHandler = cb;
    }

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

class MyGrid extends React.Component {
  constructor(props) {
    super(props);
    props.registerChangeHandler(this.changeHandler);
  }
  changeHandler = (row) => {
    ...
  }
}

In case the application already uses some form of event emitters like RxJS subjects, they can be used instead of handler.handleChange to interact between a parent and a child:

function withSubscription(WrappedComponent) {
  return class extends React.Component {
    changeEmitter = new Rx.Subject();

    handleChange = (row) => {
      this.changeEmitter.next(row);
    }

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

class MyGrid extends React.Component {
  constructor(props) {
    super(props);
    this.props.changeEmitter.subscribe(this.changeHandler);
  }

  componentWillUnmount() {
    this.props.changeEmitter.unsubscribe();
  }

  changeHandler = (row) => {
    ...
  }
}

Passing subjects/event emitters for this purpose is common in Angular because the dependency on RxJS is already imposed by the framework.

Upvotes: 2

Related Questions