-
Notifications
You must be signed in to change notification settings - Fork 30.5k
[RFC] Make all refs mutable #64772
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
[RFC] Make all refs mutable #64772
Conversation
@gaearon Thank you for submitting this PR! I see this is your first time submitting to DefinitelyTyped 👋 — I'm the local bot who will help you through the process of getting things through. This is a live comment which I will keep updated. 1 package in this PRCode ReviewsBecause this is a widely-used package, a DT maintainer will need to review it before it can be merged. You can test the changes of this PR in the Playground. Status
Once every item on this list is checked, I'll ask you for permission to merge and publish the changes. InactiveThis PR has been inactive for 31 days — it is considered abandoned, and therefore closed! Diagnostic Information: What the bot saw about this PR{
"type": "info",
"now": "-",
"pr_number": 64772,
"author": "gaearon",
"headCommitOid": "459a615b873700974dc887ba6cc0db653303fe5c",
"mergeBaseOid": "0bad336fa2bea4806c4b5d0317c52326ab50099c",
"lastPushDate": "2023-03-21T22:43:09.000Z",
"lastActivityDate": "2023-03-27T17:08:52.000Z",
"hasMergeConflict": false,
"isFirstContribution": true,
"tooManyFiles": false,
"hugeChange": false,
"popularityLevel": "Critical",
"pkgInfo": [
{
"name": "react",
"kind": "edit",
"files": [
{
"path": "types/react/index.d.ts",
"kind": "definition"
}
],
"owners": [
"johnnyreilly",
"bbenezech",
"pzavolinsky",
"ericanderson",
"DovydasNavickas",
"theruther4d",
"guilhermehubner",
"ferdaber",
"jrakotoharisoa",
"pascaloliv",
"hotell",
"franklixuefei",
"Jessidhia",
"saranshkataria",
"lukyth",
"eps1lon",
"zieka",
"dancerphil",
"dimitropoulos",
"disjukr",
"vhfmag",
"hellatan",
"priyanshurav",
"Semigradsky"
],
"addedOwners": [],
"deletedOwners": [],
"popularityLevel": "Critical"
}
],
"reviews": [
{
"type": "stale",
"reviewer": "ericanderson",
"date": "2023-03-21T20:08:07.000Z",
"abbrOid": "9bb15d2"
}
],
"mainBotCommentID": 1470935228,
"ciResult": "fail",
"ciUrl": "https://github.com/DefinitelyTyped/DefinitelyTyped/commit/459a615b873700974dc887ba6cc0db653303fe5c/checks?check_suite_id=11718564804"
} |
Hey @gaearon, 😒 Your PR doesn't modify any tests, so it's hard to know what's being fixed, and your changes might regress in the future. Please consider adding tests to cover the change you're making. Including tests allows this PR to be merged by yourself and the owners of this module. This can potentially save days of time for you! |
🔔 @johnnyreilly @bbenezech @pzavolinsky @ericanderson @DovydasNavickas @theruther4d @guilhermehubner @ferdaber @jrakotoharisoa @pascaloliv @Hotell @franklixuefei @Jessidhia @saranshkataria @lukyth @eps1lon @zieka @dancerphil @dimitropoulos @disjukr @vhfmag @hellatan @priyanshurav @Semigradsky — please review this PR in the next few days. Be sure to explicitly select |
Added another change to get rid of MutableRefObject. |
@gaearon The CI build failed! Please review the logs for more information. Once you've pushed the fixes, the build will automatically re-run. Thanks! Note: builds which are failing do not end up on the list of PRs for the DT maintainers to review. |
@gaearon You may want to leave MutableRefObject in place, but mark it as deprecated and stub it. |
Yeah I could do that! I guess one thing I have no idea about (how to check this?) is how disruptive just removing the overload would be. I imagine it would flag a lot of existing usages and force them to add Would it be better to just go for a minimal change of |
|
What would the next step here? Does anyone want to drive this change and figure out the exact plan? |
Public facing types (e.g. But I also would appreciate a retro on what our intention was and why it failed which can be reconstructed from git-blame, closed PRs/issues and https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks/#useref. This can all be done by anybody although I understand if it seems intimidating. I'll definitely help when needed. Otherwise I'll take this on next week. |
Having played with this in ts playground, while I think the types are poorly named/confusing, I do not think this change should be taken as is. That said, I do agree with the
The constraints I think we need to hold are: type Foo = { hello: string };
useRef<Foo>(null) => { current: Foo | null }; // technically correct and a documented case
useRef<Foo>(undefined) => { current: Foo | undefined }; // undocumented but it is how the react code works
useRef<Foo>() => { current: Foo | undefined }; // Undocumented and not desirable per @gaearon but technically consistent with react codebase.
useRef<Foo>({ hello: "world" }) => { current: Foo }; // Expected behavior
useRef<Foo|undefined>({ hello: "world}) => { current: Foo | undefined }; // Expected behavior
useRef({ hello: "world"} as Foo) => { current: Foo } // expected inference
useRef(null) => { current: null }; // today
useRef(null) => { current: unknown } // This may be more accurate and useful?
IFF @gaearon and the react team want to remove/discourage the use of Edit: playground link Edit 2: I no longer think changing |
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 long comment for the reasoning.
@gaearon One or more reviewers has requested changes. Please address their comments. I'll be back once they sign off or you've pushed new commits. Thank you! |
As for zero-argument case, I agree with you that it's probably quite common in the product code. It's understandable why one might write it this way (it's less to write!) and maybe that's why it's important to support. I think I'm down with keeping it if it doesn't introduce weird semantics. I would only ask that we don't use React's internal tests as a reason. React internal tests are written before features are tested and rolled out to production, so they don't always have all the right types (we don't typecheck tests at all), are written only for the team's own understanding, and have to test very weird cases too (so they shouldn't be used as canonical references).
To be clear, I didn't propose a minor bump change. I meant let's think about what we want to do for 19. And maybe there's some smaller version of that that can be done in a minor.
These look reasonable to me. To clarify, the zero-arguments case is not the one I care about deeply. I was just a bit surprised this is allowed. The part that seems weird to me is |
For what it's worth, if we take the example you linked to from the React codebase: function Example({phase}) {
const hostRef = useRef();
const classRef = useRef();
return (
<>
<div key={`host-${phase}`} ref={hostRef} />
<Component key={`class-${phase}`} ref={classRef} />
</>
);
} Then the irony is that it already does not appear to work with the current typings. Let's compress it a bit to focus on the essence: function Example() {
const hostRef = useRef();
return <div ref={hostRef} />;
} By itself, it gives me an error.
How about this one: function Example() {
const hostRef = useRef<HTMLDivElement>();
return <div ref={hostRef} />;
} Nah, still broken:
Okay, maybe let me pass function Example() {
const hostRef = useRef(null);
return <div ref={hostRef} />;
} No error! But now let's read the actual DOM ref. function Example() {
const hostRef = useRef(null);
function handleFoo() {
console.log(hostRef.current);
}
return <div ref={hostRef} />;
} No error either! But wait, my IDE tells me OK let's try this: function Example() {
const hostRef = useRef<HTMLDivElement>(null);
function handleFoo() {
console.log(hostRef.current);
}
return <div ref={hostRef} />;
} Cool, this works as expected. So as you can see, it seems like you already can't really have Not sure I agree with this myself but I wanted to clarify what irks me about how this works. |
Nor did I say you did. I am simply ensuring that when others come through here who won't go through all the comments and the git blames/prs that got us here, that we can get to a base set of understanding. A lot of people come through these types. This one in particular.
I think this makes perfect sense. We generally refer to documentation for these types of things. It wasn't the best argument in my comment.
Awesome. The initial PR didn't specify and it becomes a topic on nearly every PR to As for a change we could do now, from my ts playground link, I think we can safely remove the readonly and we can change the return type/signatures as I mostly proposed (excluding the
Modifying my proposal to add the following overload: Unfortunately, I can't think of a way to get the type later inferred correctly and this will just be sad. For 19, we could say that not specifying a generic nor an initial value gets you a default type of |
I agree
|
Updated ts playground link |
So, in plain JS it's not quite pointless — e.g. it's useful for this pattern const ref = useRef()
if (!ref.current) {
ref.current = new Thing()
} Or say const timeoutRef = useRef()
function handleStart() {
timeoutRef.current = setTimeout(...)
}
function handleStop() {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
} So it's useful for non-DOM cases where people imply it's If I understand correctly, to express this in TS, one needs to always add |
I've updated this PR — does this match your proposal? Or is there a part of your proposal that isn't reflected in this PR? I'm very new to TS so I don't quite understand how your overrides would layer. |
If we have a final proposal it would be good if I could run it by our team to verify I'm not making some silly assumptions. |
@gaearon The CI build failed! Please review the logs for more information. Once you've pushed the fixes, the build will automatically re-run. Thanks! Note: builds which are failing do not end up on the list of PRs for the DT maintainers to review. |
Agree its not pointless in plain js. But in typescript I don't think there is a clear way (currently) for the inference to be differed.
Roughly, in your example it would be like this:
which would hit this overload in my playground:
|
The previous This is what I meant that the current types are confusing. I need to tend to other things this evening but I would be happy to put up a PR tomorrow if you want. |
Ahh I see, I missed this. Yeah if you don't mind taking this on, I'd appreciate that a lot! Thanks. |
|
||
interface RefObject<T> { | ||
readonly current: T | null; | ||
current: T | null; |
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 think we still need the concept of a React-managed ref and a user-managed ref. The tests already have this case: const boolRef = useRef(false)
and it expects boolRef.current
to be a boolean
. But with only RefObject
boolRef.current
will be typed as boolean | null
. We're basically saying here that every ref needs to account for the null
value even though it might never have that value.
I think for many case you do want to have a bottom value in current
. But sometimes you also don't (or want that bottom value to be something other than null
).
So I start playing around with moving that | null
member to the actual usages i.e. refs created and managed by React will contain T | null
but everything else will just be T
#64896 for an alternate proposal. Keeping |
@gaearon I haven't seen any activity on this PR in more than three weeks, and it still has problems that prevent it from being merged. The PR will be closed on Apr 26th (in a week) if the issues aren't addressed. |
@gaearon To keep things tidy, we have to close PRs that aren't mergeable and don't have activity in the last month. No worries, though — please open a new PR if you'd like to continue with this change. Thank you! |
This probably doesn't build etc but I wanted to kick off the discussion.
In this tweet, we see two consequences of how the
useRef
types are set up:useRef(null)
"opts into"RefObject
withreadonly current
(instead ofMutableRefObject
). This is very unintuitive — there is really no special meaning tonull
as an argument except that the types claim that.Also,useRef()
without an argument probably doesn't make sense — we consider the initial value conceptually required.In this PR:
What if we remove these overloads and keep it plainuseRef<T>(initialValue: T): { current: T }
? I think that's what we originally defined it as (at least, that's how it appears in the React code). I get that it might be a PITA now but I was wondering how bad it is, and would it be worth to remove the confusing gotchas.I'm also wondering if it makes sense to keep the
MutableRefObject
vsRefObject
distinction, or not. I think I agree with @ferdaber in https://github.com/DefinitelyTyped/DefinitelyTyped/pull/30036/files#r229452449.I'd like to chat whether this is desirable. If yes, what's the path to shipping it and how much would it break. Thanks.