5

I'm constructing an element with an unknown number of children using React and I need a reference to each. Consider the following code:

class Items extends React.Component {
    constructor() {
        super();
        this.itemEls = [];
    }

    render() {
        return (
            <div>
                {
                    this.props.items.map((item, index) => {
                        return <div className="item" key={index} ref={(el) => this.itemEls.push(el)}>{item}</div>;
                    })
                }
            </div>
        );
    }

    componentDidMount() {
        console.log(this.itemEls.map((el) => el.innerHTML));
    }
}

ReactDOM.render(<Items items={["Hello", "World", "Foo", "Bar"]} />, document.getElementById('container'));
.item {
  display: inline-block;
  background: #EEE;
  border: 1px solid #DDD;
  color: #999;
  padding: 10px;
  font-family: sans-serif;
  font-weight: bold;
  margin: 10px;
  border-radius: 5px;
  cursor: default;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="container"></div>

I loop through the provided array and save a reference to each element as I go. When the component mounts, I print out a list of the element contents in the order they were added to the array. So far in all of my testing the order has been correct, but I can't find any documentation that confirms that this isn't just the most common result of a secret race condition going on in the background.

Is the order in which reference functions are called consistent?

The obvious solution here would be to pre-set the array length in componentWillMount and populate it with the index value from this.props.items.map - but for the sake of argument, let's say that's not possible in the actual situation I find myself in.

Mike Cluck
  • 31,869
  • 13
  • 80
  • 91
Sandy Gifford
  • 7,219
  • 3
  • 35
  • 65
  • interesting question. Can you elaborate on the use case for this? – azium Jan 20 '17 at 21:56
  • @azium I have a table with a variable number of cells. Once it renders I need to run `getClientBoundingBox` on any number of those cells (based on other input) to render an overlay. – Sandy Gifford Jan 20 '17 at 21:58
  • Since array order is guaranteed and `.map` iterates over the items in an array in order, and the callback function is not async, then there's no reason to suspect the `itemEls` could be populated in different order than the original `items` – pawel Jan 20 '17 at 22:00
  • @pawel OP's not talking about the call to `map` OP's talking about the order of `ref` callbacks, which is handled in react code – azium Jan 20 '17 at 22:03
  • @pawel re: "callback function is not async". I guess that's what I was uncertain about - it is a callback after all. – Sandy Gifford Jan 20 '17 at 22:04
  • @azium the docs say that "*The ref attribute takes a callback function, and the callback will be executed immediately after the component is mounted or unmounted.*" - the components mounted using `map` are obviously mounted in the correct order. – pawel Jan 20 '17 at 22:04
  • @pawel That is *exactly* what I was looking for. If you'd like to put that in an answer I'd gladly accept it – Sandy Gifford Jan 20 '17 at 22:04
  • @pawel yeah ok that makes sense – azium Jan 20 '17 at 22:06
  • @SandyGifford posted the answer with some additional info. – pawel Jan 20 '17 at 22:39

1 Answers1

4

The documentation says:

The ref attribute takes a callback function, and the callback will be executed immediately after the component is mounted or unmounted.

The components mounted using map are obviously mounted in the correct order, so I guess the ref callbacks are executed in order. If you suspect that the ref callbacks may be async I think you can verify this assumption by throwing an error from the callback - if the execution stops at first element, then it's synchronous (please someone correct me if I'm wrong).

If you're still concerned you can add the elements to array by the item index instead of push (which is probably what I'd do anyway):

this.props.items.map((item, index) => {
   return <div className="item" key={index} ref={(el) => {this.itemEls[index] = el} }>{item}</div>;
 })

This way you'll be protected against empty items which are skipped over by map, for example:

let a = [];
let b = [];
let items = ['Hello', 'World', 'Foo', , ,'Bar']; // items.length == 6
delete items[1];
items.map( i => { a.push( i ); return i });
console.log( a ); // [ "Hello", "Foo", "Bar" ] 
                  // - the index of "Foo" and "Bar" doesn't match original index
items.map( (i, idx) => { b[idx] = i; return i });
console.log( b ); // [ "Hello", <empty>, "Foo", <empty>, <empty>, "Bar" ]
                  // sparse array, but matching indexes
pawel
  • 35,827
  • 7
  • 56
  • 53