React: Children.only is inconsistent with Children.count

Created on 1 Feb 2018  Â·  6Comments  Â·  Source: facebook/react

Do you want to request a feature or report a bug?

Bug

What is the current behavior?

const children = [<div />];

React.Children.count(children);
// => 1

const child = React.Children.only(children);
// => Error('React.Children.only expected to receive a single React element child.')

Repro in CodeSandbox here: https://codesandbox.io/s/1vonwo4807

What is the expected behavior?

It's excepted that React.Children.only return the one and only element of the array (and not throw).

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

React 16.2.0

I'm not certain if this is the behavior prior to React 16 (pre-Fiber), but Fiber supports and encourages the use of fragments (i.e., arrays of elements); as such, this issue is much more likely to be encountered in React 16 onward.

Furthermore, the above code just reads like something is wrong.

_How many children do I have?_ 1.
_May I have the only child?_ No, I expected you to only have one child.
_Um, okay._

Most helpful comment

Thanks for chiming in.

I'm well aware of how to work around this limitation, but I'm questioning why the limitation exists.

As stated in the docs for React.Children:

React.Children provides utilities for dealing with the this.props.children opaque data structure.

We're supposed to treat children as opaque because the same element hierarchy can take on several representations.

For example, the following element hierarchies are the same to React:

const element = (
  <div>
    <span>Foo</span>
  </div>
);

const element = (
  <div>
    {
      [<span>Foo</span>]
    }
  </div>
);

The first of these would return <span>Foo</span> from React.Children.only, but the latter would not, despite producing the same virtual DOM.

This concern is all the more relevant given the new support in React 16 for arrays and fragments wherever an element is expected. I find it especially confusing that these are purported to be the same conceptually, but behave differently as far as Children.only is concerned:

const element = [
  <span>Foo</span>
];

const element = (
  <Fragment>
    <span>Foo</span>
  </Fragment>
);

If the answer is "Look at the type of this.props.children and manually unwrap the value," then children is no longer opaque, and I might as well ditch the React.Children utilities, especially since I'm not sure if they're going to lie to me.

I think the only justification for no-action here is a dogmatic concern for backwards compatibility.

All 6 comments

If the children has only one item, it would be ReactElement, not ReactElement[].

@meowtec is right. If you modify your CodeSandbox example from [<div>Didn't throw</div>] to <div>Didn't throw</div> it will work as expected. The inconsistency I see that is confusing, in this case, is that count works in both cases. And this is by design. Check the docs for React.Children.only for more information.

Thanks for chiming in.

I'm well aware of how to work around this limitation, but I'm questioning why the limitation exists.

As stated in the docs for React.Children:

React.Children provides utilities for dealing with the this.props.children opaque data structure.

We're supposed to treat children as opaque because the same element hierarchy can take on several representations.

For example, the following element hierarchies are the same to React:

const element = (
  <div>
    <span>Foo</span>
  </div>
);

const element = (
  <div>
    {
      [<span>Foo</span>]
    }
  </div>
);

The first of these would return <span>Foo</span> from React.Children.only, but the latter would not, despite producing the same virtual DOM.

This concern is all the more relevant given the new support in React 16 for arrays and fragments wherever an element is expected. I find it especially confusing that these are purported to be the same conceptually, but behave differently as far as Children.only is concerned:

const element = [
  <span>Foo</span>
];

const element = (
  <Fragment>
    <span>Foo</span>
  </Fragment>
);

If the answer is "Look at the type of this.props.children and manually unwrap the value," then children is no longer opaque, and I might as well ditch the React.Children utilities, especially since I'm not sure if they're going to lie to me.

I think the only justification for no-action here is a dogmatic concern for backwards compatibility.

@jamesreggio sounds reasonable to me. Either React.Children.only takes array, or Fragment not return array in this case. Either way solves confusion I think.

Any maintainers care to chime in?

React.Children utilities are in general not very consistent with each other. They're also a symptom of lacking primitives features in React — typically solutions using React.Children have other flaws and could be implemented more cleanly if React allowed some way to "call through" components. That's not to say we're never willing to change them, but it feels like the backwards compatibility cost is high, and it's not clear this is worth doing, compared to creating other more idiomatic and powerful ways to address the same use cases.

Speaking of this specific issue, it's naming that's the problem. React.Children.only wasn't intended to guarantee one child per se, but a safe downcast to ReactElement. The intention was that if you have arbitrary children, you could use React.Children.only to be sure you're dealing with an element (and get a runtime invariant if you're not). This can be handy in cases where the component doesn't support dealing with multiple children, and wants to make it explicit early. In that case even passing a single-item array should be a violation because your array might be dynamic, and you might not discover the length limitation until deploying to production. So it's handy to be able to throw on every array (as well as anything else that's not an element), and that's what React.Children.only gives you. Perhaps React.Children.toElementOrThrow() would be a more accurate name but I think that ship has sailed, and changing it now isn't worth the effort.

Again, we do want to provide a better story here. But there are more fundamental flaws in Children API (e.g. that it can't "see through" user-defined components, which, unlike the Fragment issue you pointed out, can't be worked around in userland at all) which seem more worthy to address. We'll try to avoid the same mistakes in any new APIs though.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

varghesep picture varghesep  Â·  3Comments

trusktr picture trusktr  Â·  3Comments

zpao picture zpao  Â·  3Comments

huxiaoqi567 picture huxiaoqi567  Â·  3Comments

framerate picture framerate  Â·  3Comments