How to assign refs to multiple components

I'm using React to render multiple data using array.map.
How can disable the clicked button from the list?

This is my code:

  onRunClick(act, e) {
    this.refs.btn.setAttribute("disabled", true);
  }

  render () {
    return (
      <div>
        {
          this.state.acts.map((act) => {
            let boundActRunClick = this.onRunClick.bind(this, act);

            return (
              <p key={act._id}>
                Name: {act.name}, URL(s): {act.urls}
                <button ref='btn' onClick={boundActRunClick}>Run</button>
              </p>
            )
          })
        }
      </div>
    );
  }
}

Using refs doesn't work ... I think that I can't add a state since there are multiple buttons.


Solution 1:

You should use ref callback instead of ref and also yes you need multiple refs, an array should be good

According to the docs:

React supports a special attribute that you can attach to any component. The ref attribute takes a callback function, and the callback will be executed immediately after the component is mounted or unmounted.

When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument.

ref callbacks are invoked before componentDidMount or componentDidUpdate lifecycle hooks.

Using the ref callback just to set a property on the class is a common pattern for accessing DOM elements. The preferred way is to set the property in the ref callback like in the above example. There is even a shorter way to write it: ref={input => this.textInput = input}.

String refs are a legacy and and as per the docs:

Legacy API: String Refs

If you worked with React before, you might be familiar with an older API where the ref attribute is a string, like "textInput", and the DOM node is accessed as this.refs.textInput. We advise against it because string refs have some issues, are considered legacy, and are likely to be removed in one of the future releases. If you’re currently using this.refs.textInput to access refs, we recommend the callback pattern instead.

constructor() {
    super();
    this.btn = [];
}
onRunClick(act, index, e) {
    this.btn[index].setAttribute("disabled", true);
  }

  render () {
    return (
      <div>
        {
          this.state.acts.map((act, index) => {
            let boundActRunClick = this.onRunClick.bind(this, act, index);

            return (
              <p key={act._id}>
                Name: {act.name}, URL(s): {act.urls}
                <button ref={(el) => this.btn[index] = el} onClick={boundActRunClick}>Run</button>
              </p>
            )
          })
        }
      </div>
    );
  }

Solution 2:

Like @ShubhamKhatri's answer using ref is an option. You can also achieve desired behavior with state too.

Example (Single Disabled Button Option)

class App extends Component{
  constructor() {
    super();
    this.state = {
      disabled: ''
    };
  }

  onRunClick(act, index, e) {
    this.setState({ disabled: act._id });
  }

  render() {
    return (
      <div>
        {
          this.state.acts.map((act, index) => {
            let boundActRunClick = this.onRunClick.bind(this, act, index);
            return (
              <p key={act._id}>
                Name: {act.name}, URL(s): {act.urls}
                <button disabled={this.state.disabled === act._id} onClick={boundActRunClick}>Run</button>
              </p>
            )
          })
        }
      </div>
    );
  }
}

Example (Multiple Disabled Button Option)

class App extends Component{
  constructor() {
    super();
    this.state = {
      buttons: {}
    };
  }

  onRunClick(act, index, e) {
    this.setState((prevState) => { 
      const buttons = Object.assign({}, prevState.buttons, { [act._id]: !prevState.buttons[act._id] });
      return { buttons };
    });
  }

  render() {
    return (
      <div>
        {
          this.state.acts.map((act, index) => {
            let boundActRunClick = this.onRunClick.bind(this, act, index);
            return (
              <p key={act._id}>
                Name: {act.name}, URL(s): {act.urls}
                <button disabled={this.state.buttons[act._id] || false} onClick={boundActRunClick}>Run</button>
              </p>
            )
          })
        }
      </div>
    );
  }
}

Solution 3:

For function components (React 16+), you can approach it like the following:

/* 
 * @param {Object|Function} forwardedRef callback ref function  or ref object that `refToAssign` will be assigned to
 * @param {Object} refToAssign React ref object
 */
export function assignForwardedRefs(forwardedRef, refToAssign) {
  if (forwardedRef) {
    if (typeof forwardedRef === 'function') {
      forwardedRef(refToAssign)
    } else {
      forwardedRef.current = refToAssign
    }
  }
}


function MyComponent({
  forwardedRef
}) {
   const innerRef = useRef()

   function setRef(ref) {
     assignForwardedRefs(forwardedRef, ref)
     innerRef.current = ref
   }

   return <div ref={setRef}>Hello World!</div>
}

export default React.forwardRef((props, ref) => <MyComponent {...props} forwardedRef={ref} />)

Solution 4:

You can use the npm module react-multi-ref (a tiny library by me) to do this.

import React from 'react';
import MultiRef from 'react-multi-ref';

class Foo extends React.Component {
  _actRefs = new MultiRef();

  onRunClick(act, e) {
    this._actRefs.map.get(act._id).setAttribute("disabled", true);
  }

  render () {
    return (
      <div>
        {
          this.state.acts.map((act) => {
            let boundActRunClick = this.onRunClick.bind(this, act);

            return (
              <p key={act._id}>
                Name: {act.name}, URL(s): {act.urls}
                <button ref={this._actRefs.ref(act._id)} onClick={boundActRunClick}>Run</button>
              </p>
            )
          })
        }
      </div>
    );
  }
}

Though in this specific case where you just want to change an attribute on an element, instead of using a ref you should do it through state and props on the <button> through React as in the answer by @bennygenel. But if you need to do something else (call an imperative DOM method on the button, read the value of an uncontrolled input element, read the screen position of an element, etc) then you'll need to use a ref like this.