-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Fix unsafe ref usage #4612
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,53 +22,51 @@ export interface IRefObject<T extends HTMLElement = HTMLElement> { | |
} | ||
|
||
export function isRefObject<T extends HTMLElement>(value: IRef<T> | undefined | null): value is IRefObject<T> { | ||
return value != null && typeof (value as IRefObject<T>).current !== "undefined"; | ||
return value != null && typeof value !== "function"; | ||
} | ||
|
||
export type IRefCallback<T = HTMLElement> = (ref: T | null) => any; | ||
export type IRefCallback<T extends HTMLElement = HTMLElement> = (ref: T | null) => any; | ||
|
||
export function isRefCallback<T extends HTMLElement>(value: IRef<T> | undefined | null): value is IRefCallback<T> { | ||
return typeof value === "function"; | ||
} | ||
|
||
/** | ||
* 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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the type guards here prevent this from being called with |
||
refTarget.current = ref; | ||
} else if (isRefCallback(refTarget)) { | ||
refTarget(ref); | ||
} | ||
} | ||
|
||
adidahiya marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** @deprecated use mergeRefs() instead */ | ||
export function combineRefs<T extends HTMLElement>(ref1: IRefCallback<T>, ref2: IRefCallback<T>) { | ||
return mergeRefs(ref1, ref2); | ||
} | ||
|
||
/** | ||
* Utility for merging refs into one singular callback ref. | ||
* If using in a functional component, would recomend using `useMemo` to preserve function identity. | ||
*/ | ||
export function mergeRefs<T extends HTMLElement>(...refs: Array<IRef<T> | null>): IRefCallback<T> { | ||
return value => { | ||
refs.forEach(ref => { | ||
if (isRefCallback(ref)) { | ||
ref(value); | ||
} else if (isRefObject(ref)) { | ||
ref.current = value; | ||
} | ||
setRef(ref, value); | ||
}); | ||
}; | ||
} | ||
|
||
export function getRef<T extends HTMLElement>(ref: T | IRefObject<T> | null) { | ||
export function getRef<T extends HTMLElement>(ref: T | IRefObject<T> | null): T | null { | ||
if (ref === null) { | ||
return null; | ||
} | ||
|
||
return (ref as IRefObject<T>).current ?? (ref as T); | ||
} | ||
|
||
/** | ||
* 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, ref: T | null) { | ||
if (refTarget === undefined) { | ||
return; | ||
} | ||
if (isRefObject<T>(refTarget)) { | ||
refTarget.current = ref; | ||
} else if (isRefCallback(refTarget)) { | ||
refTarget(ref); | ||
} | ||
} | ||
|
||
/** | ||
* Creates a ref handler which assigns the ref returned by React for a mounted component to a field on the target object. | ||
* The target object is usually a component class. | ||
|
@@ -78,25 +76,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 }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
refTargetKey: K, | ||
refProp: IRef<T> | undefined | null, | ||
): IRef<T>; | ||
export function refHandler<T extends HTMLElement, K extends string>( | ||
refTargetParent: { [k in K]: T | IRefObject<T> | null }, | ||
refTargetKey: K, | ||
refProp?: IRef<T> | undefined | null, | ||
) { | ||
if (isRefObject<T>(refProp)) { | ||
refTargetParent[refTargetKey] = refProp; | ||
return refProp; | ||
} | ||
): IRefCallback<T> { | ||
return (ref: T | null) => { | ||
refTargetParent[refTargetKey] = ref; | ||
if (isRefCallback(refProp)) { | ||
refProp(ref); | ||
} | ||
setRef(refProp, 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.
it is valid for
RefObject.current
to beundefined
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.