One React Component. Two HOCs. How to make setState correctly update props?

1.2k views Asked by At

Given a standard compose function and a 'div' Component, how would you write the two HOCs such that:

  • The 'div' element starts as a 20px green box, then on click, becomes a 50px blue box.
  • The concerns of - a: merging state with props, and b: triggering a state change, are handled by separate HOCs.
  • the updater HOC maps state to props, and sets a default state
  • the dispatcher HOC accepts a function to get the new state on click

The example below works to get a green box, and correctly fires the handler. The update only happens in the state of the Dispatcher HOC's state. The updater HOC's state remains unchanged, as do its props.

I'm really curious to understand what's happening. Flipping the two HOCs' order in compose causes the handler not to be set. Since they both merge in {...this.props}, that doesn't make sense to me. Guessing there's something I don't understand about how multiple HOCs merge props and state.

const HOCDispatcher = myFunc => BaseComponent => {
  return class Dispatcher extends React.Component {
    constructor(props,context){
      super(props,context);
      this.handlerFn = (event)=>{this.setState(myFunc)}
    }
    render(){
      return createElement(BaseComponent,{...this.props,onClick:this.handlerFn});
    }
  }
}

const HOCUpdater = defaultState => BaseComponent => {
  return class Updater extends React.Component {
    constructor(props,context){
      super(props,context);
      this.state = Object.assign({},defaultState,this.state);
    }
    render(){
      return createElement(BaseComponent,{...this.props,...this.state});
    }
  }
}

const MyComponent = compose(
  HOCDispatcher(()=>({
    style:{width:'50px',height:'50px',background:'blue'}
  })),
  HOCUpdater({
    style:{width:'20px',height:'20px',background:'green'}
  }),
)('div');
1

There are 1 answers

2
Amin Jafari On

If you try to simplify or compile your code in a way to a less complicated structure you can understand it better:

The initial version of MyComponent

const MyComponent= class Dispatcher extends React.Component {
  constructor(props,context){
    super(props,context);
    this.handlerFn = (event)=>{this.setState({
      style:{width:'50px',height:'50px',background:'blue'}
    })}
  }
  render(){
    return <HOCUpdater onClick={this.handlerFn}/>
  }
}

Where HOCUpdater also renders as:

class Updater extends React.Component {
  constructor(props,context){
    super(props,context);
    this.state = {
      style:{width:'20px',height:'20px',background:'green'}
    };
  }
  render(){
    return <div style:{width:'20px',height:'20px',background:'green'}/>;
  }
}

Thus rendering the green box.

After triggering the click

const MyComponent= class Dispatcher extends React.Component {
  constructor(props,context){
    super(props,context);
    this.handlerFn = (event)=>{this.setState({
      style:{width:'50px',height:'50px',background:'blue'}
    })};
    this.state= {
      style:{width:'50px',height:'50px',background:'blue'}
    };
  }
  render(){
    return <HOCUpdater onClick={this.handlerFn}/>
  }
}

If you pay attention to the render, it's still the same because this.props has not changed and it is still empty. Thus no change to the style of the box whereas the state of the Dispatcher is changed!

Did you see where you went wrong? Well, just change this.props to this.state in the Dispatcher and you'll see the magic happen.

But wait, there's more!

What happens if you have a line of code like this?

createElement('div',{
  style:{width:'50px',height:'50px',background:'blue'},
  style:{width:'20px',height:'20px',background:'green'}
});

Well, it still renders the first one (the blue box) but to avoid this try changing the render method of HOCUpdater to this:

return createElement(BaseComponent,{...this.state});

and also add a componentWillReceiveProps method, so your HOCUpdater will look like this:

const HOCUpdater = defaultState => BaseComponent => {
  return class Updater extends React.Component {
    constructor(props,context){
      super(props,context);
      this.state = Object.assign({},defaultState,this.state);
    }
    componentWillReceiveProps(nextProps){
      this.setState(nextProps);
    }
    render(){
      return createElement(BaseComponent,{...this.state});
    }
  }
}