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:
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user