React: creating and referencing dynamic component refs

13,145

Solution 1

The following keeps refs to each span inside of an array called this.spans

This works because each span adds itself to the refs array using the addSpanRef method.

getAllNames then goes through all referenced spans and grabs their textContent.

If you have sub components (instead of spans), you can then call methods on those children nodes!

var NamesList = React.createClass({

  // returns an array of names
  getAllNames() {
    return this.spans.map((span) => span.textContent);
  },

  // Add to our spans refs array
  addSpanRef (node) {
    this.spans = [...this.spans, node];
  },

  render: function() {
    this.spans = []; // create the spans refs array
    var names = ['jack','fred','bob','carl'];

    // Save a ref to "this" for the forEach loop
    var thisRef = this;
    names.forEach(function (name) {
      spans.push(<span contentEditable={true} ref={thisRef.addSpanRef}>name</span>);
    });

    return (
      <div>
        {spans}
      </div>;
    );
  }
});

ReactDOM.render(<NamesList></NamesList>, mountNode);

Solution 2

1. Quick and Dirty Solution: Read names through ref on container component

One way to read all names is to put a ref on the container, and read textcontent on childNode spans.

  componentDidMount: function() {
    var elem = this.refs.container.getDOMNode();
    var nameNodes = elem.children;
    var names = [];
    for (var i=0; i<nameNodes.length; i++) {
      names.push(nameNodes[i].textContent);
    }
    console.log(names);
  },

You can find Working codepen of this here.

Warning: The code above is dirty: the user can change the content of the span, without react knowing about the DOM change. And that is a (very) bad idea.

2. Cleaner Solution

So you will need state: the user can change the content of the spans, and react needs to know about it.
Because you also need an array of all (new) edited names, this state needs to reside at container level.
Here is a solution with pure <EditableSpan> components, which call a method on their parent each time a name is changed.

var EditableSpan = React.createClass({
  onChange: function() {
    var newContent = event.target.textContent;
    this.props.onChange(this.props.index, newContent);
  },
  render: function() {
    return <span 
             contentEditable={true} 
             onInput={this.onChange}>
      {this.props.name}</span>
  }
});

Please note that this cleaner solution no longer uses refs, because they are not needed: all data is known to react, so react does not need refs to read from the DOM. Instead, it reads the event.target on every change, to update container state.

You can find full working codepen here.

Caveat: for this solution I have added quick and dirty keys (the names). React needs keys which are unique (and NOT the index). Because the names in the list are not guaranteed to be unique, you may need another smarter solution for this (IDs or timestamps of creation would do).

Share:
13,145
joshuakcockrell
Author by

joshuakcockrell

Former Software Engineer @ Robinhood, Twitter, Tesla, Microsoft, Adobe

Updated on June 04, 2022

Comments

  • joshuakcockrell
    joshuakcockrell almost 2 years

    I'm trying to keep track of an arbitrary amount of sub components.

    Normally you would just reference this.refs.refName, but in my case I have an arbitrary amount of refs I need to keep track of.

    Here's a concise example:

    var NamesList = React.createClass({
      getAllNames: function() {
        // Somehow return an array of names...
    
        var names = [];
        this.refs.????.forEach(function (span) {
          names.push(span.textContent);
        })
      },
    
      render: function() {
        var names = ['jack','fred','bob','carl'];
        var spans = [];
    
        names.forEach(function (name) {
          spans.push(<span contentEditable={true} ref='?????'>name</span>);
        });
    
        return (
          <div>
            {spans}
          </div>;
        );
      }
    });
    
    ReactDOM.render(<NamesList></NamesList>, mountNode);
    

    If I'm approaching the problem incorrectly let me know. My desired outcome is to pass in data from a RESTful service to a React component, allow the user to edit that data, and export it again when needed. I've been unable to find an answer to this in the React refs docs.

  • joshuakcockrell
    joshuakcockrell about 8 years
    Sorry, I'm trying to pull the names out of the spans dynamically. It looks like the changes you made are about setting spans dynamically. In pseudocode: for each span, grab the textContent and return these as an array. Is that helpful to explain what I'm asking?
  • wintvelt
    wintvelt about 8 years
    Updated the answer with 2 solutions. Please note that the second solution does return an array with the textContent of the spans, but without using refs. React is and should be the only owner and manager of the textContent. So it is not necessary (and source of buggy/ unmanageable code) to use refs to get the names.