-
Notifications
You must be signed in to change notification settings - Fork 53
feat(Ref): support of forwardRef()
API
#491
Conversation
Codecov Report
@@ Coverage Diff @@
## master #491 +/- ##
=========================================
Coverage ? 88.28%
=========================================
Files ? 42
Lines ? 1468
Branches ? 190
=========================================
Hits ? 1296
Misses ? 167
Partials ? 5
Continue to review full report at Codecov.
|
import React from 'react' | ||
import { Grid, Ref, Segment } from '@stardust-ui/react' | ||
|
||
const ExampleButton = React.forwardRef<HTMLButtonElement>((props, ref) => ( |
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.
The usage of generic in docs :( It's impossible to omit it there.
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.
yes, and this is definitely not the problem of this PR :) seems that we should use JS engine to process this code, as docs are aimed to present JS variant of examples to the client (this is the case for now, at least)
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.
I'm planning to left this thing as is and push #495 that will resolve all our issues.
@@ -90,6 +90,7 @@ | |||
"lodash": "^4.17.10", | |||
"prop-types": "^15.6.1", | |||
"react-fela": "^7.2.0", | |||
"react-is": "^16.6.3", |
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.
(!) A new dependency.
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.
👍
…stardust-ui/react into feat/ref-forward # Conflicts: # yarn.lock
forwardRef()
APIforwardRef()
API
We can separate this logic obviously to two subcomponents /cc @miroslavstastny |
docs/src/examples/components/Ref/Types/RefExampleForwardRef.tsx
Outdated
Show resolved
Hide resolved
src/components/Ref/Ref.tsx
Outdated
} | ||
} | ||
|
||
private handleRefOverride = (node: HTMLElement) => { |
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.
according to ref's docs: https://reactjs.org/docs/refs-and-the-dom.html
The function receives the React component instance or HTML DOM element as its argument, which can be stored and accessed elsewhere.
The parameter type used in this function leads to false assumptions.
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.
See my comment below, we will enforce user to pass correct refs.
</div> | ||
)) | ||
|
||
class RefExampleForwardRef extends React.Component { |
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.
actually, very confused by trying to understand how Ref
component is helpful in case of ref forwarding (and this, as stated in the description, is its main purpose: This PR is redesign of Ref component to use forwardRef API by default and use findDOMNode() only as a fallback.
)
Let me explain resaons for this by following the example. Suppose that I am a Stardust client and have the following component:
const MyButton = React.forwardRef((props, ref) => (
<div>
<button {...props} ref={ref} />
</div>
))
With this component I already expect the following to be true:
// when mounted, this.ref.current will refer to <button />
<MyButton ref={this.ref} />
Then comes Stardust and proposes to use Ref
- with no clear benefits to me, as now it will be either:
- I will use
Ref
as another wrapper - but what are the benefits of that :/ ?
const WithRef = (props) => (
<Ref innerRef={props.forwardedRef}>
{ React.forwardRef((props, ref) => <MyButton ref={ref} />
</Ref>
)
- or what is the scenario where I might need it?
Ref
, in case of ref forwarding case, acts as a simple ref
translator - so, what are the reasons I wouldn't use built-in React ref forwarding capabilities and would prefer Ref
component abstraction instead?
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.
Have expressed points of general concerns/questions related to the approach.
Also, to me it seems wrong that we are exposing different semantics for innerRef
for these cases:
- forward ref case
// element can be React Component reference (this) or DOM element
<Ref innerRef={ref => .. }>
...
</Ref>
- findDOMNode() case
// DOM element is always guaranteed to be returned (and never React component ref)
// semantics is absolutely different from react ref
<Ref innerRef={domNode => ..}>
...
</Ref>
Consider the following potential implementation for render method of some component - you would never able to tell which logic will be fired:
<Ref innerRef={ (?) => ... }>
<SomeComponent />
</Ref>
Have expressed these concerns earlier here: #459 (comment). Also, React docs have warnings even about just introducing ref forwarding (https://reactjs.org/docs/forwarding-refs.html) - this is clearly might be a breaking change; and with this implicit logic switch we will make problem even worse:
…stardust-ui/react into feat/ref-forward # Conflicts: # src/components/Ref/Ref.tsx
…at/ref-forward # Conflicts: # CHANGELOG.md
I think this analysis is too surface for two reasons. GitHub stars are not a good indicator of the latest best practices or the best ideas. The current state of a repo is not a good indicator of the current direction or views of the maintainers and contributors. Material UINote that Material UI provides a RootRef component that always returns a DOM node: It is also worth noting that the primary voices in the ref discussion are the React Bootstrap maintainers. You can see there that their intent is that the Ref component pattern should always return a DOM node, not a class instance. Note the use of Also, if you read Material UI's issues and PRs about Refs, you'll see they are struggling with the same problems we are. Specifically, you can see the community issues complaining that their refs don't support forwardRef. You can also see their PR where they've updated their refs to do so mui/material-ui#11757. Note, they check for an existing ref, whether the ref has a ref (ie it is a class), handle functional refs explicitly, and also handle forwardRef (ref.current) explicitly: handleRefInput = node => {
this.input = node;
let ref;
if (this.props.inputRef) {
ref = this.props.inputRef;
} else if (this.props.inputProps && this.props.inputProps.ref) {
ref = this.props.inputProps.ref;
}
if (ref) {
if (typeof ref === 'function') {
ref(node);
} else {
ref.current = node;
}
}
}; React BootstrapNote that React Bootstrap also solve this problem by exposing props like The point I'm making is that on the surface your argument seems correct, however, if you dig deeper and allow more time and real-world usage to pass you will find that the useful thing about refs is getting DOM nodes. User's don't want class instances and, as much as possible, they don't want special cases. |
const StyledButton = React.forwardRef((props, ref) => <MaterialUIButton ref={ref} style={...} {...props} /> @kuzhelov the provided case is fully invalid, you're forwarding ref to component that doesn't support this feature. It's a shot in the foot 💣 Users want to have ref forwarding: mui/material-ui#13221 (issue marked as important) And the same problems with deprecation: |
yes, but the key difference from our Speaking of the analysis taken:
I was aimed to challenge the following statement:
So, that was the motivation for the approach to analysis I've taken, and to my mind its results loudly prove that statement above doesn't reflect current state of things.
when this code is written by the same person (and, thus, entirely visible) - yes, it is. But consider the case when
// this what client's code now is:
import { wrapped } from 'wrappers'
const Foo = () => ...
export wrapped(Foo) and, suddenly, after that everything has stopped to work. Is it clear for the client that she has done something that might shoot her leg? Don't think so |
@kuzhelov it is not quite that solid. The Material UI I believe we are all saying the same thing in regard to the problems that are present. Let's turn to solutions at this point. What are the optimal solutions to these issues? I would start with the statement that returning a class is not a solution, but a problem as well. Our goals are:
Let's shift our focus to APIs and patterns for meeting these goals. |
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.
I vote for merging this. Let's iterate when we hit the wall.
…stardust-ui/react into feat/ref-forward # Conflicts: # CHANGELOG.md
Speaking of Material's case provided here: #491 (comment) - please, note that this is about
Note, this is completely different from the Stardust's case where there is possibility for any client component to be used as a slot content.
There is, actually - and this will be about following forward refs chain and do Note that all the implementations we are considering from other libraries, as well as the one that is proposed by this PR - those still largely rely on the |
Thanks for the correction on my misreading of materials' input. I think I've become a bit hasty in trying to close this issue. Let's move on. |
This PR is a next stage for #417.
findDOMNode()
is deprecated inStrictMode
, ref. and we can't rely on it, it also forces us to usereact-dom
that means that it will be impossible to use it with React Native.This PR is redesign of
Ref
component to useforwardRef
API by default and usefindDOMNode()
only as a fallback. If React's team will decide to removefindDOMNode()
it will be not issue for us.