1

I am having trouble figuring out what the best strategy is to preserve my original component's function (specifically setSelected() when wrapping it into a higher order function with Redux's connect(). I am making use of a great set of components for radio buttons for my frontend app:

Radio.js:

class Radio extends React.Component {
  constructor(props) {
    super(props);
    this.state = {selected: props.selected};
  }

  toggle() {
    console.log(this.context)
    const {onChange} = this.context.radioGroup;
    const selected = !this.state.selected;
    this.setState({selected});
  //  this.props.changeRadioGroupState(this.props.option);
    onChange(selected, this);
  }

  setSelected(selected) {
    this.setState({selected});

  }


  render() {

    const activeStyle = {
      opacity: 0.1
    }
    const grayStyle = {
      opacity: 0.5
    }
    let classname = 'form__big_button'
    return (
      <button type="button"
      className={classname}
      onClick={this.toggle.bind(this)}
      style={this.state.selected ? activeStyle : grayStyle }>
        {this.props.option}
      </button>
    );
  }
}


Radio.contextTypes = {
  radioGroup: React.PropTypes.object
};

It has a parent component called RadioGroup.js:

import React from 'react';

class RadioGroup extends React.Component {
  constructor(props) {
    super(props);
    this.options = [];

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

  getChildContext() {
    const {name} = this.props;
    return {radioGroup: {
      name,
      onChange: this.onChange.bind(this)
    }};
  }

  changeRadioGroupState(selectedOption){
     this.setState({selectedOption: selectedOption});
  }

  onChange(selected, child) {
    console.log(this.options)
    this.options.forEach(option => {
      if (option !== child) {
        option.setSelected(!selected);
      }
    });
  }

  render() {
    let children = React.Children.map(this.props.children, child => {


      let newElement = React.cloneElement(child, {
        ref: (component => {this.options.push(component);})
      });


      return newElement;
    });
    return <div className="radio-group">{children}</div>;
  }
}

RadioGroup.childContextTypes = {
  radioGroup: React.PropTypes.object
};

export default RadioGroup;

Together, it's rendered very simply:

<RadioGroup>
  <Radio id="1" selected={true} option="Opt1"/>
  <Radio id="2" selected={false} option="Opt2"/>
</RadioGroup>

However, I want to wire up Radio to my Redux store so I can update the selected radio button:

const mapStateToProps = (state) => {
  return {searchType: state.searchType}
}


export default connect(mapStateToProps)(Radio);

This results, however, in an error on the browser:

Uncaught TypeError: option.setSelected is not a function
    at eval (RadioGroup.js?6ccb:27)
    at Array.forEach (<anonymous>)
    at RadioGroup.onChange (RadioGroup.js?6ccb:25)
    at Radio.toggle (Radio.js?9dd4:17)
    at Object.ReactErrorUtils.invokeGuardedCallback (ReactErrorUtils.js?9efc:71)
    at executeDispatch (EventPluginUtils.js?68fe:79)
    at Object.executeDispatchesInOrder (EventPluginUtils.js?68fe:102)
    at executeDispatchesAndRelease (EventPluginHub.js?daec:43)
    at executeDispatchesAndReleaseTopLevel (EventPluginHub.js?daec:54)
    at Array.forEach (<anonymous>)

This is clearly a result of the connect() component wrapping my Radio component- it no longer exposes a setSelected() function. What is the best way to preserve my original component's functions when wrapping it in a higher order component with the Redux connect() function?

I've tried this.setSelected = this.setSelected.bind(this) binding in the constructor, but it looks like the connect() function returns a completely different type of object- a Connect object.

Yu Chen
  • 6,540
  • 6
  • 51
  • 86

1 Answers1

4

As you are using Redux and using ref, you should be passing to connect the options parameter withRef (it is the fourth parameter). Your connect will look like this: export default connect(mapStateToProps, null, null, { withRef: true })(Radio);

Then, you will be able to get to the functions of the component with: option.getWrappedInstance().setSelected(!selected);

You can have a look at the react-redux docs here

But I think that this approach isn't a very good one, I wouldn't recommend overusing refs, maybe try to pass the selected as a prop from the parent RadioGroup in order to get a neater solution.

Matan Bobi
  • 2,693
  • 1
  • 15
  • 27
  • This worked beautifully. Thank you. Could you explain a bit why you don't recommend overusing refs? – Yu Chen Apr 04 '18 at 14:33
  • 1
    Sure. mostly its because its a way to modify a child outside the regular lifecycle. It's a little like a jQuery approach which isn't really recommended. You can have a look at the react docs https://reactjs.org/docs/refs-and-the-dom.html#dont-overuse-refs and see what they say about it – Matan Bobi Apr 04 '18 at 14:40