-
Notifications
You must be signed in to change notification settings - Fork 350
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
Guard against matchMedia access during initial render #1459
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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@khanacademy/perseus": patch | ||
--- | ||
|
||
Guard against executing matchMedia during initial render |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,6 +109,7 @@ | |
|
||
// The rendered markers on the question image for labeling. | ||
_markers: Array<Marker | null | undefined>; | ||
_mounted: boolean = false; | ||
|
||
static gradeMarker(marker: InteractiveMarkerType): InteractiveMarkerScore { | ||
const score = { | ||
|
@@ -375,6 +376,14 @@ | |
}; | ||
} | ||
|
||
componentDidMount() { | ||
this._mounted = true; | ||
} | ||
|
||
componentWillUnmount() { | ||
this._mounted = false; | ||
} | ||
|
||
simpleValidate(rubric: LabelImageProps): PerseusScore { | ||
return LabelImage.validate(this.getUserInput(), rubric); | ||
} | ||
|
@@ -503,17 +512,17 @@ | |
|
||
const {activeMarkerIndex, markersInteracted} = this.state; | ||
|
||
// Render all markers for widget. | ||
return markers.map((marker, index): React.ReactElement => { | ||
// Determine whether page is rendered in a narrow browser window. | ||
const isNarrowPage = window.matchMedia( | ||
mediaQueries.xsOrSmaller.replace("@media ", ""), | ||
).matches; | ||
// Determine whether page is rendered in a narrow browser window. | ||
const isNarrowPage = | ||
this._mounted && | ||
window.matchMedia(mediaQueries.xsOrSmaller.replace("@media ", "")) | ||
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 usage doesn't use a helper to guard 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. Guarding against it being The |
||
.matches; | ||
Comment on lines
+517
to
+519
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. note: Moved out of the loop since it won't change for each item, and modified to return |
||
|
||
// Determine whether the image is wider than it is tall. | ||
const isWideImage = | ||
this.props.imageWidth / 2 > this.props.imageHeight; | ||
// Determine whether the image is wider than it is tall. | ||
const isWideImage = this.props.imageWidth / 2 > this.props.imageHeight; | ||
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. note: Moved out of the loop; doesn't need recalculating multiple times. 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. Thanks for fixing this up! |
||
|
||
// Render all markers for widget. | ||
return markers.map((marker, index): React.ReactElement => { | ||
let side: "bottom" | "left" | "right" | "top"; | ||
let markerPosition; | ||
// Position popup closest to the center, preferring it renders | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,10 @@ | |
}; | ||
|
||
function shouldReduceMotion(): boolean { | ||
// We cannot use matchMedia during SSR. | ||
if (typeof window.matchMedia !== "function") { | ||
return true; | ||
} | ||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); | ||
return !mediaQuery || mediaQuery.matches; | ||
} | ||
|
@@ -49,13 +53,22 @@ | |
|
||
// The marker icon element. | ||
_icon: HTMLElement | null | undefined; | ||
_mounted: boolean = false; | ||
|
||
static defaultProps: { | ||
selected: ReadonlyArray<any>; | ||
} = { | ||
selected: [], | ||
}; | ||
|
||
componentDidMount() { | ||
this._mounted = true; | ||
} | ||
|
||
componentWillUnmount() { | ||
this._mounted = false; | ||
} | ||
|
||
renderIcon() { | ||
const {selected, showCorrectness, showSelected, showPulsate} = | ||
this.props; | ||
|
@@ -106,7 +119,7 @@ | |
} else if (showPulsate) { | ||
iconStyles = [ | ||
styles.markerPulsateBase, | ||
shouldReduceMotion() | ||
this._mounted && shouldReduceMotion() | ||
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. note: We don't want animation on by default on initial render in case the user has turned it off (long term, it would be useful if this also could be overridden by webapp profile setting too). |
||
? showPulsate && styles.markerUnfilledPulsateOnce | ||
: showPulsate && styles.markerUnfilledPulsateInfinite, | ||
]; | ||
|
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.
note: We don't want animation on by default on initial render in case the user has turned it off (long term, it would be useful if this also could be overridden by webapp profile setting too).
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 confused how this fixes things. The
mediaQueryIsMatched
function guards againstwindow.matchMedia
not being a function. Is that helper written wrong or is the fix more about only running on a mounted 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.
This is a subtle thing, but we don't just want to guard against it being accessed; the initial render on both server and client must be identical, so we don't want to use
matchMedia
at all on first render as the client could give a different rendered result than the server.