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

Fix unsafe ref usage #4612

Merged
merged 2 commits into from Apr 7, 2021
Merged

Conversation

maclockard
Copy link
Contributor

Fixes #4611

Checklist

  • Includes tests
  • Update documentation

Changes proposed in this pull request:

This changes a couple aspects of ref usage.

  • Makes it so that refHandler always creates a ref callback and always assigns an element to the local target.
    • This makes refHandler (more) type safe to use
    • This also prevents components from incorrectly accessing the local target element.
  • Updates the local ref handler when a new ref callback is passed.
    • Previously, when passing a new ref callback it would only get called once, while the old ref callback would continue getting called.
  • Properly set the previously passed ref to null, according to https://reactjs.org/docs/refs-and-the-dom.html#caveats-with-callback-refs

Reviewers should focus on:

Changes to the ref utility functions and component lifecycle methods.

return value != null && typeof (value as IRefObject<T>).current !== "undefined";
return value != null && typeof value !== "function";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

it is valid for RefObject.current to be undefined when using refs for something other than HTML elements. This pattern is much more common when using functional components. While Blueprint doesn't really use FCs yet, and the type constraint here should prevent it, this change helps future proof.

* Assign the given ref to a target, either a React ref object or a callback which takes the ref as its first argument.
*/
export function setRef<T extends HTMLElement>(refTarget: IRef<T> | undefined | null, ref: T | null): void {
if (isRefObject<T>(refTarget)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the type guards here prevent this from being called with undefined | null, so I removed the undefined check

@@ -78,25 +71,10 @@ export function setRef<T extends HTMLElement>(refTarget: IRef<T> | undefined, re
export function refHandler<T extends HTMLElement, K extends string>(
refTargetParent: { [k in K]: T | null },
refTargetKey: K,
): IRefCallback<T>;
export function refHandler<T extends HTMLElement, K extends string>(
refTargetParent: { [k in K]: T | IRefObject<T> | null },
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This type made it pretty easy to cause runtime errors since it does not require the passed type to include IRefObject<T>. The problem here being that k here should be contravariant, but since typescript doesn't allow for this it defaults to being covariant.

This kind of typing issue is pretty common with refs and Typescript, and leads to things like the bivariance hack in the base react types.

More detail here: #4611

@@ -212,7 +211,7 @@ export class DateInput extends AbstractPureComponent2<IDateInputProps, IDateInpu
if (
e.key === "Tab" &&
!e.shiftKey &&
this.lastTabbableElement.classList.contains(Classes.DATEPICKER_DAY)
this.lastTabbableElement?.classList.contains(Classes.DATEPICKER_DAY)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TBH, I'm not sure why the type checker wasn't catching this null call here. It might be worth doing a deeper pass of all current usage of refs to make sure they are properly null checked.

Copy link
Contributor

Choose a reason for hiding this comment

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

not all packages use strict null checks, unfortunately. this one has that compiler option turned off at the moment.

Copy link
Contributor Author

@maclockard maclockard Apr 7, 2021

Choose a reason for hiding this comment

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

gotcha, makes sense. I'm so used to having that compiler option turned on, I forgot TS doesn't check that by default haha

Copy link
Contributor

@adidahiya adidahiya left a comment

Choose a reason for hiding this comment

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

lgtm aside from the breaking change

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

Successfully merging this pull request may close these issues.

refHandler not typesafe safe and causes components to break when passed object ref
2 participants