3

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.

3 Answers 3

2

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);
Sign up to request clarification or add additional context in comments.

Comments

1

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).

2 Comments

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?
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.
-1

This is probably the simplest way to create dynamic refs that I have found.

Also this solution works without any issues from TypeScript as well.

const Child = props => <input ref={refElem => setRef(props.someKey, refElem)} />

class Parent extends Component {

    setRef = (key, ref) => {
      this[key] = ref; // Once this function fires, I know about my child :)
    };

    render(){
        return (
          {myList.map(listItem => <Child someKey={listItem.key} setRef={this.setRef} />)}
        )
    }
}

Anyways hope this helps someone.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.