Is [my use of] react-bootstrap's popover "the react way"?

1.8k views Asked by At

I'm working on an React app, my first one, and I'm struggling with how to implement user-interactive popovers. I have a business requirement to have a popover with yes/no buttons in it. This popover is triggered by a button ("pobtn") that lives in a row in a table in a view:

+- View ---------------------+          +- View ---------------------+
| +- Table ----------------+ |   click  | +- Table ----------------+ |
| | +- Row --------------+ | |    -->   | | +- Row --------------+ | |
| | | ...                | | |   pobtn  | | | ...                | | |
| | +--------------------+ | |          | | +--------------------+ | |
| | +- Row --------------+ | |   ,--------. +- Row --------------+ | |
| | | ...        [pobtn] | | |   | Yes/No |>| ...        [pobtn] | | |
| | +--------------------+ | |   `--------' +--------------------+ | |
| | +- Row --------------+ | |          | | +- Row --------------+ | |
| | | ...                | | |          | | | ...                | | |
| | +--------------------+ | |          | | +--------------------+ | |
| | ...                    | |          | | ...                    | |
| +------------------------+ |          | +------------------------+ |
+----------------------------+          +----------------------------+

The View, Table and Row are all components. Table data is fetched in View and pushed to Table via props, and that trickles down into Row, which I understand is "the right way" to do it, and makes sense. There isn't much local state there.

The trouble comes with the popover. The "right way" to do it would seem to include a Popover component along side each button and push props down for hidden/visible, I guess. This doesn't seem super great but is closer to the right way to do it in my mind.

However, the react-bootstrap popover (which I used because time) doesn't seem to work this way:

var content = <MyRowPopoverContent/>;
<OverlayTrigger trigger='click' placement='left' overlay={<Popover>{content}</Popover>
    <button className="btn btn-primary small"><i className="fa fa-trash-o"></i> Delete</button>
</OverlayTrigger>

When you click the trigger button, it creates a DIV attached to the body and positions itself next to the triggering element, similar to how it works normally (non-react). It looks like I might be able to attach the DIV to another container, but that isn't documented very well. Anyway, the overlay content (i.e., MyRowPopoverContent) is something like:

this.handleNoClick: function(){
  // Re-click the button to close; gross.
  $('[data-row-id="'+this.props.row.key+'"]').find('.fa-trash-o').parents('button').first().click();
},
this.handleYesClick: function(){
  // Do stuff...update store which would re-render the view.
  // Re-click the button to close; gross.
  $('[data-row-id="'+this.props.row.key+'"]').find('.fa-trash-o').parents('button').first().click();
},
this.render: function(){
  <div>
    <div>Are you sure?</div>
    <input placeholder="Reason"/>
    <button onClick={this.handleYesClick}>Yes</button> <button onClick={this.handleNoClick}>No</button>
  </div>
}

I started implementing the handleYesClick function, trying to do error handling, asynchronous events, etc., and it all seems really sketchy, or at least fragile. I guess the question is: I need an interactive popover, how do I do that? It seems like the popup would be part of the local state of the button or row, or maybe this creeps up to the top as most things seem to do.

Since react-bootstrap already provides one, it would be nice to use that, but I'm not sure how to "hook it up" with everything else.

Update: If it were a simple static popover that would be cake; showing/hiding it via props and clicking pobtn is pretty trivial. The part I struggle with is the interactive content within the popover--having that do something (like an event to update the store), show a spinner, determine if that something worked, show an error message or disappear. Even clicking the "No" button and making the popover disappear isn't clear (I currently find it with jQuery and .click() the button again.) Doing that all via props from the parent seems scary...

1

There are 1 answers

0
akarve On

Suppose you encapsulate your popover into a PopoverMenu component. Here's a React-friendly pattern where a parent handles its child's events:

var Table = React.createClass({
  handlePopoverClick(selection, meta) {
    // selection - the selection ('Yes' or 'No' in your case)
    // meta - anything you want to know about the caller
  },
  render: function() {
    var open = /* boolean logic to determine if popover is open or closed* /;
    // key idea is to pass the child a handler from the parent
    <PopoverMenu handler={this.handlePopoverClick} open={open} ... />
    ...
  }
});

var PopoverMenu = React.createClass({
  handleClick: function(selection, meta) {
    this.props.handler(selection, meta);
  },
  render: function() {
    ...
    //somewhere you render the 'Yes' Button of this popover
    <Button onClick={this.handleClick.bind(this, 'Yes', {row: 2})} />
  }
});

The PopoverMenu component is stateless; it knows just enough to render. Its handler receives the click event and then adjusts PopoverMenu's props as needed. The magic is in .bind, which allows you to inject caller-specific information for the handler to react to. (You can imagine that the .bind occurs in a map call where Yes and {row :2} are dynamic variables.) Note that you can handle all popover choices with a single handler in the parent, and that you can easily track, as part of a parent's state (e.g. Row), the waiting/success/failure state of any calls. This may seem scary but said logic needs to live somewhere :) and it's better if that somewhere is not the child, since the React way is stateless children. State naturally becomes props as info flows from parent to child.

That said, if your parent is doing too much book-keeping and you aren't getting clean, self-contained behavior from the child components, you can push logic down into the children, usually at the cost of child reusability. It is both reasonable and possible for the child to handle its own open/close state and waiting/error/success state. If you go this route, one pattern which you can use is to put the child in a waiting state until and unless you receive a matching value from the parent via props. For example, if the user makes a menu selection 'X' that requires a callback, the child does setState{ selection: 'X'} and waits for the parent to corroborate via props (which reflect the state of the store):

if (this.state.selection !== this.props.selection) {
  //render component as waiting
} else {
  //render as normal
}

Regarding the open/close status of the popover, it would be easier if you instead used a component like ButtonDropdown, which manages its own open/close state. (You may have to set an empty onSelect handler on the dropdown for it to close automatically upon selection.)