Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add React.Children.isRenderable #11

Closed
wants to merge 4 commits into from
Closed

Add React.Children.isRenderable #11

wants to merge 4 commits into from

Conversation

ngub
Copy link

@ngub ngub commented Jan 11, 2018

This RFC specifies a React.Children.isRenderable method which allows checking whether a child component (or it's children) return a node or doesn't return anything.

const MaybeWrapper = ({children}) =>
  React.Children.map(children, child) => 
    React.Children.isRenderable(child)
      ? <Wrapper>{child}</Wrapper>
      : child


# Detailed design

A method takes a `child` component instance, finds it's corresponding fiber and

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's -> its


# Basic example

`React.Children.isRenderable` lets us easily detect children without UI.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would perhaps revise the naming to something like React.Children.hasRenderedOutput. Components that render nothing are still "renderable" if that makes sense.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

@thysultan
Copy link

How is this different from isValidElement?

@ngub
Copy link
Author

ngub commented Jan 11, 2018

@thysultan isValidElement checks whether an object is a React element while the proposed method checks whether an instance of React component renders any HTML.

@milesj
Copy link

milesj commented Jan 11, 2018

I'm not sure how feasible this is, as it would require rendering the component with a set of props to determine whether it actually renders something. Especially considering that the return of a component can change between renders and the props passed.

@ngub
Copy link
Author

ngub commented Jan 11, 2018

@milesj I'm sorry if it's not clear from the RFC. This method accepts an instantiated component (a React element) and not a class or a function. This is how other methods from React.Children namespace work. You can read more about React elements in this great article.

@bvaughn
Copy link
Collaborator

bvaughn commented Jan 20, 2018

I wonder if you've seen Ryan's call-return experiment?

@ngub
Copy link
Author

ngub commented Feb 19, 2018

@bvaughn, thank you for the link! I am not sure if I got the idea of call-return clearly (the video is not available anymore and all mentions lead to this video), and the use case doesn't seem obvious from the code. Although I noticed those isCall and isReturn functions which check the instance's type. I couldn't find much information about these symbol types either and while logging my components I get an object instead of a Symbol type. Could you please unwrap the experiment's concept or provide another resource? Thanks!

```js
const MaybeWrapper = ({children}) =>
React.Children.map(children, child =>
React.Children.isRenderable(child)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why you want this method on React.children and not React.isRenderable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be convenient when we use it for whatever is passed to children prop, although my own example provides a workaround for this situation. I agree, that this is an arguable detail.


Why should we *not* do this? Please consider:

- the proposed feature can be implemented in user space
Copy link

@gnapse gnapse Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on how would this be achievable in user space? Perhaps if it can be shown that it would be complex enough to do so, it could help your case.

For instance, it may be naivety on my side, but I'm not sure how isRenderable(child) is different than simply doing child != null. Perhaps this will detect the rendering of blank strings and return false for those too? Or perhaps it'll do the same that that condition does, but more efficiently? I'm not entirely familiar with fiber internals to fully make sense of the details in the "Detailed design" section above.

@gaearon
Copy link
Member

gaearon commented Aug 18, 2021

Thanks for the RFC! I realize this is a pretty late review, see #182 on some discussion around that.

We'd like to not expand the React.Children namespace. While it's occasionally very useful, it's also a flawed feature. Unlike other React features, it doesn't allow composition. Specifically, components that use React.Children don't take well to, for example, wrapping a bunch of children into a single component. We're keeping it around because we don't have a better solution for these cases, but in general we don't plan to change React.Children and hope to eventually make it unnecessary.

Your RFC appears to be more ambitious — it would actually check children "deeply". This is not how any of the existing React.Children utilities work so it probably doesn't belong there. Regardless, there are some big questions about the design that are unsolved by this proposal. I will close it because it's not workable as is, but maybe somebody would open a follow-up.

A method takes a child component instance, finds it's corresponding fiber and checks whether it has a stateNode. If a stateNode exist, the method returns true, else it starts recursively repeat this process on a child fiber and it's siblings until it finds a non-empty stateNode containing a DOM node, otherwise it returns false.

I don't think this works. You're describing a check against the tree that already exists. But the code executes for the tree that is currently being rendered. That could be a completely different tree from the one that already exists — it could have different props or structure. So this implementation doesn't solve the problem and would be inconsistent. There is also a flaw in that it assumes you can get an internal instance (a fiber) based on a child. But that's not how React or JSX works — the child itself is only a description. It's the place into which you place it in the output — its position — that determines the associated internal instance.

This method accepts an instantiated component (a React element) and not a class or a function.

An element is not an "instantiated component", it's only an object like { type: Foo }. I hope this clarifies it. <Foo /> is not the result of executing Foo deeply — it is merely a description. React lazily descends the tree. This is why isRenderable(<Foo />) can't work unless it actually attempts to render Foo deeply which brings a whole suite of other questions:

  • How do you preserve context? It's normally determined by a place in the tree. If you do isRenderable(<Foo />) but then place it in a <Wrapper>, and Wrapper has a context provider in it, how do you ensure the <Foo /> execution during the check also gets the same context? It wasn't wrapped in the provider yet.
  • How do you reuse the rendered result? Elements like <Foo /> are stateless, they are merely descriptions. So how do you make it so that the result of the initial "deep render" of <Foo /> is actually being used in the output? You wouldn't want to render it twice.
  • How "deep" does this deep render go? Does it happen synchronously? This compromises on other features, like time slicing, where we try to distribute the work. We use the per-component boundary to check for interruptions, but this feature would have to be synchronous and recursive so that wouldn't work.
  • Checking for null specifically seems like a low return on investment for such an invasive feature. The request here is to add a "pierce-through" capability which has rippling implications on many other features. It doesn't seem worth it just to find out whether a component renders null. What are the other use cases that "pierce-through" capability would solve? This seems like a piece of a much more general proposal, but that proposal needs fleshing out.

I am not sure if I got the idea of call-return clearly (the video is not available anymore and all mentions lead to this video), and the use case doesn't seem obvious from the code.

Call/Return is our much earlier exploration of this idea. Here's roughly how it worked. A child component could return a special value, called a "return". E.g. return createReturn(10). And a parent component could return a special value, called a "call". E.g. return createCall(children, ...). The second argument there is a callback, in which the parent would receive collected "returns" from its children. For example if you have child <A /> that renders createReturn(10) instead of JSX, and a child <B /> that renders createReturn(20), their parent could render createCall(<><A /><B /></>, (returns) => <div>{returns[0] + returns[1]}</div>) and that would become <div>30</div>. Crucially, you could extract <><A /><B /></> into a separate component like <MyChildren /> and it would still work. One way to look at this feature is that usually, child return values are opaque to the parent — they're just put into its JSX — but this feature let you actually "call" components and use their "returns".

This feature kind of worked but we thought it's pretty confusing (however, it did specify solutions to the problems outlined above). So we did not proceed with that API and ended up deleting it. But a future attempt to bring something like this back would probably need to similarly "powerful" but also much more usable.


I'll also add a comment from @sebmarkbage from when we were last discussing this feature internally:

There are many nuances to this problem space:

  • Whether you can do multiple passes over the same components and how you communicate that.
  • Whether you can use a different set of children in the first pass vs the second pass.
  • Whether you can reorder children between the passes.
  • Whether it leaks multiple levels or terminates at some predictable point.
  • Where state/error/context propagation happens.

Then there are implementation details like what's the overhead of these.
It's possible to do these in user space with hooks since hooks are composable.

function Foo({children}) {
  // synchronously reconciles against state stored in useState
  // enough if the tree is small and then rendering continues below
  let yields = useChildren(children);
  return <div>{doSomething(yields)}</div>;
}

The most natural way to do getDerivedStateFromCatch with hooks might be a generator form:

function *Foo({ children }) {
  let [showError, updateError] = useState(false);
  if (showError) {
    return <Error retry={() => updateError(false)} />;
  }
  try {
    return yield children;
  } catch (x) {
    updateError(true);
  }
}

This would also naturally perhaps provide a point for call/return.

function *Foo({children}) {
  let yields = yield children; // asynchronously reconciles
  return <div>{doSomething(yields)}</div>;
}

Those APIs alone doesn't allow for the multi-pass model perhaps depending on how we model it though.
It's a big space but I don't think we should bring back the model that we had.

I think the way the generator form would work is that we just reconcile the tree again every time it yields against the same children. When it returns, we know we’re done. That lets you do multiple passes against the same subtree until you stabilize. That might be a different use case than the model where the first and second pass touches different components.

I think what Seb meant by it being possible in userspace is that technically React Hooks are implemented using a "dispatcher" which we switch during runtime to the appropriate implementation. So a Hook like useChildren above could temporarily hijack the dispatcher to the one that provides "in-place" implementations of other Hooks. I haven't really thought about this, but in the spirit of transparency, that's the current thinking/feedback on this class of proposals.

I'll close this proposal, since in the current form it's very unlikely to happen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants