92

I'm essentially trying to make tabs in react, but with some issues.

Here's file page.jsx

<RadioGroup>
    <Button title="A" />
    <Button title="B" />
</RadioGroup>

When you click on button A, the RadioGroup component needs to de-select button B.

"Selected" just means a className from a state or property

Here's RadioGroup.jsx:

module.exports = React.createClass({

    onChange: function( e ) {
        // How to modify children properties here???
    },

    render: function() {
        return (<div onChange={this.onChange}>
            {this.props.children}
        </div>);
    }

});

The source of Button.jsx doesn't really matter, it has a regular HTML radio button that triggers the native DOM onChange event

The expected flow is:

  • Click on Button "A"
  • Button "A" triggers onChange, native DOM event, which bubbles up to RadioGroup
  • RadioGroup onChange listener is called
  • RadioGroup needs to de-select button B. This is my question.

Here's the main problem I'm encountering: I cannot move <Button>s into RadioGroup, because the structure of this is such that the children are arbitrary. That is, the markup could be

<RadioGroup>
    <Button title="A" />
    <Button title="B" />
</RadioGroup>

or

<RadioGroup>
    <OtherThing title="A" />
    <OtherThing title="B" />
</RadioGroup>

I've tried a few things.

Attempt: In RadioGroup's onChange handler:

React.Children.forEach( this.props.children, function( child ) {

    // Set the selected state of each child to be if the underlying <input>
    // value matches the child's value

    child.setState({ selected: child.props.value === e.target.value });

});

Problem:

Invalid access to component property "setState" on exports at the top
level. See react-warning-descriptors . Use a static method
instead: <exports />.type.setState(...)

Attempt: In RadioGroup's onChange handler:

React.Children.forEach( this.props.children, function( child ) {

    child.props.selected = child.props.value === e.target.value;

});

Problem: Nothing happens, even I give the Button class a componentWillReceiveProps method


Attempt: I attempted to pass some specific state of the parent to the children, so I can just update the parent state and have the children respond automatically. In the render function of RadioGroup:

React.Children.forEach( this.props.children, function( item ) {
    this.transferPropsTo( item );
}, this);

Problem:

Failed to make request: Error: Invariant Violation: exports: You can't call
transferPropsTo() on a component that you don't own, exports. This usually
means you are calling transferPropsTo() on a component passed in as props
or children.

Bad solution #1: Use react-addons.js cloneWithProps method to clone the children at render time in RadioGroup to be able to pass them properties

Bad solution #2: Implement an abstraction around HTML / JSX so that I can pass in the properties dynamically (kill me):

<RadioGroup items=[
    { type: Button, title: 'A' },
    { type: Button, title: 'B' }
]; />

And then in RadioGroup dynamically build these buttons.

This question doesn't help me because I need to render my children without knowing what they are

Andy Ray
  • 30,372
  • 14
  • 101
  • 138
  • If the children can be arbitrary, then how could the `RadioGroup` possibly know it needs to react to an event of its arbitrary children? It necessarily has to know something about its children. – Ross Allen Aug 16 '14 at 20:44
  • 1
    In general, if you want to modify the properties of a component you don't own, clone it using [`React.addons.cloneWithProps`](https://facebook.github.io/react/docs/clone-with-props.html) and pass the new props you'd like it to have. `props` are immutable, so you are creating a new hash, merging it with the current props, and passing the new hash as `props` to a new instance of the child component. – Ross Allen Aug 16 '14 at 20:46

4 Answers4

43

I am not sure why you say that using cloneWithProps is a bad solution, but here is a working example using it.

var Hello = React.createClass({
    render: function() {
        return <div>Hello {this.props.name}</div>;
    }
});

var App = React.createClass({
    render: function() {
        return (
            <Group ref="buttonGroup">
                <Button key={1} name="Component A"/>
                <Button key={2} name="Component B"/>
                <Button key={3} name="Component C"/>
            </Group>
        );
    }
});

var Group = React.createClass({
    getInitialState: function() {
        return {
            selectedItem: null
        };
    },

    selectItem: function(item) {
        this.setState({
            selectedItem: item
        });
    },

    render: function() {
        var selectedKey = (this.state.selectedItem && this.state.selectedItem.props.key) || null;
        var children = this.props.children.map(function(item, i) {
            var isSelected = item.props.key === selectedKey;
            return React.addons.cloneWithProps(item, {
                isSelected: isSelected,
                selectItem: this.selectItem,
                key: item.props.key
            });
        }, this);

        return (
            <div>
                <strong>Selected:</strong> {this.state.selectedItem ? this.state.selectedItem.props.name : 'None'}
                <hr/>
                {children}
            </div>
        );
    }

});

var Button = React.createClass({
    handleClick: function() {
        this.props.selectItem(this);
    },

    render: function() {
        var selected = this.props.isSelected;
        return (
            <div
                onClick={this.handleClick}
                className={selected ? "selected" : ""}
            >
                {this.props.name} ({this.props.key}) {selected ? "<---" : ""}
            </div>
        );
    }

});


React.renderComponent(<App />, document.body);

Here's a jsFiddle showing it in action.

EDIT: here's a more complete example with dynamic tab content : jsFiddle

ericn
  • 12,476
  • 16
  • 84
  • 127
Claude Précourt
  • 907
  • 11
  • 11
  • 17
    Note: cloneWithProps is deprecated. Use React.cloneElement instead. – Chen-Tsu Lin Jul 18 '15 at 06:07
  • 8
    This is so unbelievably complicated to do something D3/Jquery could do with just a :not selector on `this`. Is there any real advantage, other than having used React? – Union find Sep 29 '15 at 02:26
  • FYI.. this is not "updating children state from the parent state", this is child updating the parent state in order to update the children state. There's a difference in the verbage here. In case this confuses people. – sksallaj May 12 '16 at 00:56
  • 1
    @Incodeveritas you're right, sibling relationship, parent-child, and child-parent is a bit complicated. This is a modular way of doing things. But keep in mind JQuery is a quick hack to get things done, ReactJS keeps things according to an orchestration. – sksallaj May 12 '16 at 00:58
18

The buttons should be stateless. Instead of updating a button's properties explicitly, just update the Group's own state and re-render. The Group's render method should then look at its state when rendering the buttons and pass "active" (or something) only to the active button.

jackcogdill
  • 4,900
  • 3
  • 30
  • 48
  • To expand, the button onClick or other relevant methods should be set from the parent so that any action calls back into the parent to set state there, not in the button itself. That way the button is just a stateless representation of the parents current state. – David Mårtensson Nov 13 '18 at 08:40
6

Maybe mine is a strange solution, but why do not use observer pattern?

RadioGroup.jsx

module.exports = React.createClass({
buttonSetters: [],
regSetter: function(v){
   buttonSetters.push(v);
},
handleChange: function(e) {
   // ...
   var name = e.target.name; //or name
   this.buttonSetters.forEach(function(v){
      if(v.name != name) v.setState(false);
   });
},
render: function() {
  return (
    <div>
      <Button title="A" regSetter={this.regSetter} onChange={handleChange}/>
      <Button title="B" regSetter={this.regSetter} onChange={handleChange} />
    </div>
  );
});

Button.jsx

module.exports = React.createClass({

    onChange: function( e ) {
        // How to modify children properties here???
    },
    componentDidMount: function() {
         this.props.regSetter({name:this.props.title,setState:this.setState});
    },
    onChange:function() {
         this.props.onChange();
    },
    render: function() {
        return (<div onChange={this.onChange}>
            <input element .../>
        </div>);
    }

});

maybe you require something else, but I found this very powerfull,

I really prefer to use an outer model that provide observer register methods for various tasks

David Schumann
  • 13,380
  • 9
  • 75
  • 96
Daniele Cruciani
  • 623
  • 1
  • 8
  • 15
  • Isn't it bad form for a node to be explicitly calling setState on another node? Would a more reactive way be to update some state on the RadioGroup, triggering another render pass in which you could update the props of the descendants as necessary? – qix Dec 03 '15 at 21:14
  • 1
    Explicitly? No, observer just call a callback, it happens to be the setState, but in fact I could have specified this.setState.bind(this) and so being exclusively bindend to itself. Ok, if I had to be honest I should had defined a local function/method that call setState and registered it in the observer – Daniele Cruciani Dec 04 '15 at 22:20
  • 1
    Am I reading this wrong, or do you have two fields with the same name in one object for `Button.jsx`? – JohnK May 17 '16 at 21:46
  • Button.jsx includes `onChange()` and `onChange(e)` – Josh Apr 21 '17 at 12:31
  • remove the first, it is cut&paste from the question (first question). It is not it the point, this is not a working code, neither it would be. – Daniele Cruciani Apr 24 '17 at 07:17
0

Create an object that acts as a middleman between the parent and child. This object contains function references in both the parent and child. The object is then passed as a prop from the parent to the child. Code example here:

https://stackoverflow.com/a/61674406/753632

Johann
  • 27,536
  • 39
  • 165
  • 279