Wrap all alert payloads in useMemo to avoid infinite re-rendering (#182)

* Wrap all alert payloads in useMemo to avoid infinite re-rendering

This manifested in production as a browser freeze for any
user who saw the 15%-off-to-upgrade message.

TNL-7400

* fixup! meant to say identity, not equality
This commit is contained in:
Kyle McCormick
2020-08-12 17:12:31 -04:00
committed by GitHub
parent cc7142e5c1
commit bc76adf8eb
6 changed files with 37 additions and 25 deletions

View File

@@ -7,11 +7,9 @@ function useAccessExpirationAlert(courseExpiredMessage, topic) {
const rawHtml = courseExpiredMessage || null; const rawHtml = courseExpiredMessage || null;
const isVisible = !!rawHtml; // If it exists, show it. const isVisible = !!rawHtml; // If it exists, show it.
const payload = useMemo(() => ({ rawHtml }), [rawHtml]);
useAlert(isVisible, { useAlert(isVisible, {
code: 'clientAccessExpirationAlert', code: 'clientAccessExpirationAlert',
payload, payload: useMemo(() => ({ rawHtml }), [rawHtml]),
topic, topic,
}); });

View File

@@ -1,6 +1,6 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import React, { import React, {
useContext, useState, useCallback, useContext, useState, useCallback, useMemo,
} from 'react'; } from 'react';
import { UserMessagesContext, ALERT_TYPES, useAlert } from '../../generic/user-messages'; import { UserMessagesContext, ALERT_TYPES, useAlert } from '../../generic/user-messages';
@@ -14,15 +14,16 @@ export function useEnrollmentAlert(courseId) {
const course = useModel('courses', courseId); const course = useModel('courses', courseId);
const outline = useModel('outline', courseId); const outline = useModel('outline', courseId);
const isVisible = course && course.isEnrolled !== undefined && !course.isEnrolled; const isVisible = course && course.isEnrolled !== undefined && !course.isEnrolled;
const payload = {
canEnroll: outline.enrollAlert.canEnroll,
courseId,
extraText: outline.enrollAlert.extraText,
isStaff: course.isStaff,
};
useAlert(isVisible, { useAlert(isVisible, {
code: 'clientEnrollmentAlert', code: 'clientEnrollmentAlert',
payload: { payload: useMemo(() => payload, Object.values(payload).sort()),
canEnroll: outline.enrollAlert.canEnroll,
courseId,
extraText: outline.enrollAlert.extraText,
isStaff: course.isStaff,
},
topic: 'outline', topic: 'outline',
}); });

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import { useAlert } from '../../generic/user-messages'; import { useAlert } from '../../generic/user-messages';
const OfferAlert = React.lazy(() => import('./OfferAlert')); const OfferAlert = React.lazy(() => import('./OfferAlert'));
@@ -10,7 +10,7 @@ export function useOfferAlert(offerHtml, topic) {
useAlert(isVisible, { useAlert(isVisible, {
code: 'clientOfferAlert', code: 'clientOfferAlert',
topic, topic,
payload: { rawHtml }, payload: useMemo(() => ({ rawHtml }), [rawHtml]),
}); });
return { clientOfferAlert: OfferAlert }; return { clientOfferAlert: OfferAlert };

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useAlert } from '../../../../generic/user-messages'; import { useAlert } from '../../../../generic/user-messages';
@@ -23,14 +23,15 @@ function useCertificateAvailableAlert(courseId) {
const endDate = endBlock ? new Date(endBlock.date) : null; const endDate = endBlock ? new Date(endBlock.date) : null;
const hasEnded = endBlock ? endDate < new Date() : false; const hasEnded = endBlock ? endDate < new Date() : false;
const isVisible = isEnrolled && certBlock && hasEnded; // only show if we're between end and cert dates const isVisible = isEnrolled && certBlock && hasEnded; // only show if we're between end and cert dates
const payload = {
certDate: certBlock && certBlock.date,
username,
userTimezone,
};
useAlert(isVisible, { useAlert(isVisible, {
code: 'clientCertificateAvailableAlert', code: 'clientCertificateAvailableAlert',
payload: { payload: useMemo(() => payload, Object.values(payload).sort()),
certDate: certBlock && certBlock.date,
username,
userTimezone,
},
topic: 'outline-course-alerts', topic: 'outline-course-alerts',
}); });

View File

@@ -1,5 +1,5 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import React from 'react'; import React, { useMemo } from 'react';
import { useAlert } from '../../../../generic/user-messages'; import { useAlert } from '../../../../generic/user-messages';
import { useModel } from '../../../../generic/model-store'; import { useModel } from '../../../../generic/model-store';
@@ -23,15 +23,16 @@ export function useCourseEndAlert(courseId) {
const endDate = endBlock ? new Date(endBlock.date) : null; const endDate = endBlock ? new Date(endBlock.date) : null;
const delta = endBlock ? endDate - new Date() : 0; const delta = endBlock ? endDate - new Date() : 0;
const isVisible = isEnrolled && endBlock && delta > 0 && delta < WARNING_PERIOD_MS; const isVisible = isEnrolled && endBlock && delta > 0 && delta < WARNING_PERIOD_MS;
const payload = {
delta,
description: endBlock && endBlock.description,
endDate: endBlock && endBlock.date,
userTimezone,
};
useAlert(isVisible, { useAlert(isVisible, {
code: 'clientCourseEndAlert', code: 'clientCourseEndAlert',
payload: { payload: useMemo(() => payload, Object.values(payload).sort()),
delta,
description: endBlock && endBlock.description,
endDate: endBlock && endBlock.date,
userTimezone,
},
topic: 'outline-course-alerts', topic: 'outline-course-alerts',
}); });

View File

@@ -8,6 +8,17 @@ export function useAlert(isVisible, {
const { add, remove } = useContext(UserMessagesContext); const { add, remove } = useContext(UserMessagesContext);
const [alertId, setAlertId] = useState(null); const [alertId, setAlertId] = useState(null);
// Please note:
// The deps list [isVisible, code, ... etc.] in this `useEffect` call prevents the
// effect from running if none of deps have changed. However, "changed" for objects is
// defined in terms of identity; thus, if you provide a payload that is *seemingly* equal
// to the previous one but *actually* a different object, then this effect will run.
// If you are particularly unlucky, this will cause an infinite re-render loop.
// This manifested itself in TNL-7400.
// We hope to address the underlying issue in TNL-7418.
// In the mean time, you may follow the pattern that `useAccessExpirationAlert`
// establishes: memoize the payload so that the exact same object is used if the
// payload has not changed.
useEffect(() => { useEffect(() => {
if (isVisible && alertId === null) { if (isVisible && alertId === null) {
setAlertId(add({ setAlertId(add({