-
Notifications
You must be signed in to change notification settings - Fork 559
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
Fragment refs RFC #97
Conversation
Will there be short syntax version? <ref={fragmentRef}>
</> |
@streamich No. There are already other cases where the |
Thank you @sebmarkbage ! 🙏
Maybe i'm not reading this well, but when you say Based on your example:
Will it "catch" the Maybe my real question is, will fragment ref be able to support All capabilities |
A text node is also a DOM node so yes.
Yes.
Is there some capability that you feel is missing? Just to clarify, |
I think the biggest issue here is it's no longer possible to get a DOM node for an arbitrary instance ref. Sure you can put a fragment around a third party component and acquire some DOM node from it, but then it's guesswork and hacky workarounds to find the appropriate subtree within that parent. This is particularly problematic for layered HOCs with interim DOM where the thing you actually want may be quite deep in the hierarchy. |
Is this meant to also cover some of the use cases of |
Can you share some thoughts around how this API will be the same or different for React Native |
Not sure, for my personal use cases it seems to cover them (will need to dig deeper though )
Strange, if you wrap a child that returns a string: |
this is awesome 👍 Definitely addresses most of the potential concerns i've had with deprecating findDOMNode cc @taion |
|
||
# Alternatives | ||
|
||
The alternative to this API is simply to not add anything and instead encourage libraries to use custom elements as wrappers. E.g. `x-fragment`. These can use tricks like `display: contents` to play nicer into layouts. Ideally the DOM would support something like this first class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Migration concerns mean this is probably infeasible, but something like a better state of the world would have been a convention like components exposing some prop like hostRef
to enable their parents to access their host nodes, in an opt-in manner.
So in that sense, a new API isn't strictly necessary, if we envision "access to host node" as an opt-in thing; but with my library maintainer hat on, I really would not want to live in that world.
Just pointing this out for completeness.
I'm working on code splitting things with suspense and things are breaking because of the findDOMNode calls in react-node-resolver Slightly related to this: @jossmac, i think you'll like this, reactjs/rfcs#97
* Use render prop for target in withModalHandlers I'm working on code splitting things with suspense and things are breaking because of the findDOMNode calls in react-node-resolver Slightly related to this: @jossmac, i think you'll like this, reactjs/rfcs#97 * Update types * Fix linting error * Fix a thing * Fix thing
* Add lazy loading stuff to field-views-loader * More stuff * Use render prop for target in withModalHandlers I'm working on code splitting things with suspense and things are breaking because of the findDOMNode calls in react-node-resolver Slightly related to this: @jossmac, i think you'll like this, reactjs/rfcs#97 * Update types * Fix linting error * Fix a thing * Fix thing * Works in some cases * Fix a thing * Add a Suspense component around page content * Update CreateItemModal in Relationship field type * Update UpdateManyItemsModal * Everything is _mostly_ working now * Fix a thing * Fix some things * forwardRef Pill * Fix things * Fix a thing * Add changeset * Add changeset * Add changeset for @arch-ui/layout changes * Add `compression` and change webpack config stuff so it works how it will work when it's on npm as a test * Change webpack stuff back * Try a thing to fix the access control tests * Only do gzipping in prod
What is the status of this RFC, I am struggling with API designs. |
I hope this feature can be implemented sooner. This is the only reason for me that cant not get rid of class components. |
Isn't returning a Array of children quite a departure on how the current DOM refs (which point to a single DOM node) and I'm happy to have it return a Array of children, just a bit surprised by the fact that it does so. Strictly speaking, I guess it should be called |
@silverwind IMO it should still be called |
Ok, so I've been researching use cases for findDOMNode or something like Fragment refs - and I don't think this is the right direction. Taking a deeper look was originally spawned from seeing that fragment refs ends up being quite confusing when it comes to hidden offscreen content (such as prerendered, display locked or hidden due to a suspense boundary). You may end up getting more nodes than you'd expect. More over, it is too tempting to just assume or assert a single child node while not considering multi-node implementations like suspense boundaries. EncapsulationThe fundamental benefits we get from React's component model is the constraints it enforces so that you can trust how other components behave. E.g. when their life-cycles fire, that they get there input from props/context etc. A parent can know something about what its children does, but a child shouldn't need to know about its parents. It's a critical property that you can refactor children without also refactoring all its uses. That empowers reuse. A key feature of that is being able to change the implementation details by using multiple DOM nodes or wrapping the root component in a Suspense boundary that renders two nodes, or rendering a fragment with one in-place DOM node and one portal. It's also about knowing which properties a parent is allowed to override by accepting explicit props or context to manage those overrides. Neither findDOMNode nor Fragment refs preserve that quality. Since they give full access to the DOM node. The net effect is that components have to resort to defensive programming. E.g. adding an early wrapper just in case someone drills into the component. It also adds a risk that discourages refactoring such components. forwardRefforwardRef doesn't have this problem, as much, since it's an explicit reference that you opt-in to provide a public API that includes a particular DOM node. It also allows the ref to be redirected to a specific place so it can change position during refactors. You can also use named ref props to allow multiple refs. Unfortunately it also encourages all these components to expose their entire DOM node. It's a reasonable escape hatch but not ideal. Ultimately I think we'll want to move towards not needing this as much. For example, if we had more like first-class focus management or something "scopes" based way of setting focus then components wouldn't need to expose a directly focusable thing. For now it's a decent workaround though. Use CasesUse cases for findDOMNode tends to fall into roughly these categories:
Adding additiona properties or styles to a child is generally considered bad and not expected since it may collide with the styles already defined in a component. There's generally good intuition about this but it sometimes breaks down. E.g. is it ok to add an outline to a child component's styles when it's focused? What if there's two children in a fragment? Typically when this happens, it's a use case that could be better solved by a wrapper DOM element that takes on the size of the children. In fact, all these cases can be solved by adding a wrapper DOM node. I'd argue that any use case that couldn't be solved by a wrapper DOM node is not legit since it risks conflicting with the child. Even for things like event listeners, that can have two at once, this causes problems with unclear invocation order. It's not obvious when an event listener will get attached. E.g. normally a parent's life-cycle fire later so it probably attaches an event listener that is invoked after the child. However, if you call useEffect to attach and remove event listeners, then changes to the deps in the child may cause it to readd a new listener that is now invoked after the parent. You also have to be careful to also call stopImmediatePropagation at the right places. It's just much simpler if the event bubbling model is always related to parent and child. Therefore, I argue that the default solution to all these problems should be to add a wrapper DOM node. Why Not Wrapper DOM Elements?In practice, it can be difficult to add a wrapper Element in the DOM for a number of reasons:
The performance considerations for DOM nodes are real, but keep in mind that we're talking about a small linear increase. It's not exponential. In general when DOM nodes are too many it's because either windowing (virtualization) is not used, or you have too many complex CSS selectors. I'd also expect that the overall effect would be fewer DOM nodes or the same after a while given that reusable components now don't have to use as much defensive coding. Regarding CSS child selectors, if abused in this way, they have the same encapsulation problems as findDOMNode. I'd argue that they shouldn't be used at all in components system and if used they should be restricted to not drill through the encapsulation. Even outside React, this is what Shadow DOM is for. If this remains a problem, maybe we should consider rendering components into Shadow DOM so that these abuses are not possible. Defining layout of a wrapper can be tricky. Luckily HTML parsing and SSR is a problem that doesn't have an obvious solution in user space right now. The typical solution is to provide an optional tag name that can be provided in the cases this component is used in unusual contexts like a table. Alternative 1: Reified Fragment NodeI think the ideal would be that the browser adds a first-class fragment tag that solves these issues by default. However, until then it's possible that we could build a polyfill in React. One possible idea is that when you render To solve the HTML parsing problem, React could omit the fragment tags during HTML rendering in contexts where it's not allowed. Then during hydration it could add them back in. This works because when you imperatively append content to the DOM, invalid tags are allowed to exist in the tree. Alternative 2: Virtual Fragment NodeSome things, like rendering an additional outline around a component, will need a real reified element. However many use cases are not adding additional behavior to the DOM. They're just observing something about the DOM. These typically fall into two categories:
Events are pretty easy to do in React since we already have a virtual event system. We can add additional levels and bubble through a fragment just as if it was really in the DOM without reifing it. <Fragment onClick={e => e.stopPropagation()}>...</Fragment> The only thing is that, unlike a reified fragment, this wouldn't capture events on text nodes. You'd have to add an explicit wrapper for that. Similarly, we could measure the client rectangle anytime the children resizes: <Fragment onResize={boundingRect => ...}>...</Fragment> When I say "bounding rect" I really mean some abstraction over the whole stacking context. So that you can figure out if it's obscured by other boxes. It might also need to be multiple rectangles when they're not adjacent such as the rectangle of a portal. The use cases that require the bounding box of the child includes:
The bounding box is special because it is a primitive contract of a component. Every component ultimately takes up space in the layout of its parent (or zero if it's a portal/modal). This size can be trivially observed, e.g. adding a reified wrapper that sizes itself to its child and then measure that. This type of use case doesn't break encapsulation because you can completely change the implementation (including rendering a canvas even without DOM nodes) as long as it takes up layout somehow. I think a lot of people have found this intuition given that these are the primary use cases where people use findDOMNode today. So this is a very specific use case that we could add support for without giving full access to the DOM nodes. Literally having an |
I'm going to close this RFC out since I'm pretty convinced that this particular implementation is not a good idea. It doesn't seem super critical as a way to move off findDOMNode. We've found that it is possible to migrate off it with some inconvenience and that's also what we've heard from the community. I still think one or two of the alternatives I mentioned above would be good additions to React as a further convenience that doesn't break encapsulation. I encourage someone to write up an RFC for each of the alternatives. |
Thanks @sebmarkbage for the research and detailed explanation. Virtual fragments sounds interesting. |
I would like to voice some support for Alternative 2 as it would solve our use-case of measuring the bounding rects of components in an arbitrary React tree of third-party components. Solutions that introduce wrapping with reified elements are not compatible with what we need, but virtual nodes would work very similarly to |
when i use run that code, it is warning that:
How can I make it run successfully? |
This RFC just closed |
Summary
This is an alternative to findDOMNode.
Basic example
Rendered text