1

I have the following component, that contains a dropdown whose items open a modal, by calling a method on a child component:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.modal = React.createRef();
  }

  render() {
    ...

    let dropdownItems;

    if (isCreate) {
      dropdownItems = <>
        { /* Calling open() on the child component */ }
        <Dropdown.Item onClick={() => {this.modal.current.open()}}>Create</Dropdown.Item>
      </>;
    } else {
      dropdownItems = <>
        { /* Same thing, from another item */ }
        <Dropdown.Item onClick={() => {this.modal.current.open()}}>Edit</Dropdown.Item>
        <Dropdown.Item>Delete</Dropdown.Item>
      </>;
    }

    return (
      <Dropdown>
        <Dropdown.Toggle as={CustomDropdownToggle} id={dropdownId} />
          <Dropdown.Menu>
            <Dropdown.Header>...</Dropdown.Header>

            {dropdownItems}

            { /* Storing a reference to the child component */ }
            <EditMatchModal ref={this.modal} productMatch={productMatch} />
          </Dropdown.Menu>
      </Dropdown>
    );
  }
}
class EditMatchModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOpen: false };
  }

  open() {
    this.setState({ isOpen: true });
    this.loadData();
  }

  loadData() {
    // Perform AJAX request...
  }

  render() {
    return (
      <Modal>
        ...
      </Modal>
    );
  }
}

This works fine, however I read here and in the docs that refs are an escape hatch, that should usually be avoided.

What's a better alternative to avoid the usage of refs in this case?

One solution would be to move the isOpen property to the parent MyComponent, but:

  • it really feels like it belongs to the modal component
  • when changed, it must immediately trigger an AJAX request, so it makes sense to have the method located on the modal where the AJAX code lies

Another solution would be to move the buttons that open the modal to the EditMatchModal itself, but:

  • there are 2 buttons, so this would duplicate the modal component (is this a big deal?)
  • the buttons are not just buttons, but <Dropdown> items, so the presentation of the parent component would leak into the child component, which is a big smell to me

Did I miss something?

BenMorel
  • 34,448
  • 50
  • 182
  • 322

1 Answers1

1

I disagree that the isOpen propery belongs to the modal. It is best to lift the state up to the nearest common parent component. For example, the nearest common parent component for <Dropdown.Item /> and <Modal /> is your parent component, <MyComponent />. You can have the onClick method of <Dropdown.Item /> look like this:

<Dropdown.Item onClick={this.toggleModal}>Edit</Dropdown.Item>
// In MyComponent
state = {
  modalIsOpen: false
}

toggleModal = () => {
  this.setState({
     modalIsOpen: true
  })
}

And then conditionally render your modal based on the state:

{this.state.modalIsOpen && <EditMatchModal productMatch={productMatch} /> }

You may need a method within the modal itself to close the modal (or anywhere else that you want to close the modal), which would require a callback that sets state.modalIsOpen to false.

This is much cleaner. You don't need the ref, or the method within the modal itself. It is clear to see from the parent when the modal should be open or not open. It is easy to affect its status by affecting state. If any other children need to open the modal, you'll have to provide them with a callback that calls back to this.toggleModal. Or if a component way down the tree needs to affect it, you should consider a more beefy state management tactic.

As for making an ajax call, you can still keep any ajax calls in the componentDidMount of your modal.

Edit: For React-BootStrap

In the comments it came to light that this is using React-Bootstrap (RB). In which case you don't need to conditionally render the <Modal /> - RB handles that for you. Your method should still be a method of the parent as described above, but now you can render your modal like this:

// In MyComponent
<EditMatchModal show={this.state.modalIsOpen} productMatch={productMatch} />

// EditMatchModal
class EditMatchModal extends React.Component {

  loadData() {
    // Perform AJAX request...
  }

  render() {
    return (
      <Modal show={this.props.show}>
        ...
      </Modal>
    );
  }
}
Seth Lutske
  • 9,154
  • 5
  • 29
  • 78
  • Thank you, I guess this makes sense. I'm just feeling a bit uncomfortable with conditionally outputting a modal with ``, instead of always outputting it, with `` (I'm wondering if it's designed to be added to / removed from the DOM all the time; although it seems to work fine). – BenMorel Feb 13 '20 at 07:42
  • Once you have the `` in a conditional render, why would you still need the `show` prop at all? – Seth Lutske Feb 13 '20 at 16:18
  • I don't know, this makes me wonder why this `show` property exists in the [Modal](https://react-bootstrap.github.io/components/modal/) component then? – BenMorel Feb 13 '20 at 16:26
  • Oooohh, you're using *react bootstrap*...you didn't mention that anywhere in your question or tags. If you read the documentation on the site you linked, you can see that it says "Modals are positioned over everything else in the document and remove scroll from the `` so that modal content scrolls instead." So the modal is always present in order to properly modify the document `

    ` styles based on the `show` prop. But it also says "Modals are unmounted when closed." So a bootstrap modal is really also conditionally rendering everything *within* its ` ... ` tags.

    – Seth Lutske Feb 13 '20 at 16:43
  • But even so, your `isOpen` propery should still belong to the parent of the ``, and can then be passed as the `show` prop to the ``, and then down to the `` within that. It shouldn't rely on a method within itself to decide whether or not to render. – Seth Lutske Feb 13 '20 at 16:46
  • Sorry for not mentioning React Bootstrap, it felt unnecessary when I asked the question! What's the point of passing the `show` prop, if it's always going to be `true` when the component is rendered anyway? – BenMorel Feb 13 '20 at 21:36
  • I agree, that doesn't make sense. React-bootstrap handles the conditional rendering for you. I adjusted my answer. – Seth Lutske Feb 13 '20 at 22:53
  • Thank you, this works fine as well. I didn't know that `` conditionally rendered everything inside it. So all in all, I guess both approaches are 100% equivalent? – BenMorel Feb 14 '20 at 09:57
  • I ended up reverting to conditionally rendering the whole `EditMatchModal`, as you originally suggested, with ``. The reason is, [I have to manually focus an input in `componentDidMount()`](https://stackoverflow.com/q/60216787/759866) (`` steals the focus for some reason, killing `autoFocus`), but without the conditional rendering, `componentDidMount()` would fire on page load, instead of when opening the modal. – BenMorel Feb 14 '20 at 12:27