From 9936d7a024b727c5a4fbbb5a3b4a0a2c83f64905 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:16:51 +0000 Subject: [PATCH 1/6] chore(deps): update dependency eslint-plugin-react-hooks to v7 --- frontends/package.json | 2 +- yarn.lock | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/frontends/package.json b/frontends/package.json index 4824764354..d337b69ac9 100644 --- a/frontends/package.json +++ b/frontends/package.json @@ -54,7 +54,7 @@ "eslint-plugin-jest": "^29.0.0", "eslint-plugin-mdx": "^3.0.0", "eslint-plugin-react": "^7.34.3", - "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-styled-components-a11y": "^2.1.35", "eslint-plugin-testing-library": "^7.0.0", "jest": "^29.7.0", diff --git a/yarn.lock b/yarn.lock index aafd3a01fc..69ec505904 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12097,15 +12097,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react-hooks@npm:^5.0.0": - version: 5.1.0 - resolution: "eslint-plugin-react-hooks@npm:5.1.0" - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - checksum: 10/b6778fd9e1940b06868921309e8b269426e17eda555816d4b71def4dcf0572de1199fdb627ac09ce42160b9569a93cd9b0fd81b740ab4df98205461c53997a43 - languageName: node - linkType: hard - "eslint-plugin-react-hooks@npm:^7.0.0": version: 7.0.1 resolution: "eslint-plugin-react-hooks@npm:7.0.1" @@ -12865,7 +12856,7 @@ __metadata: eslint-plugin-jest: "npm:^29.0.0" eslint-plugin-mdx: "npm:^3.0.0" eslint-plugin-react: "npm:^7.34.3" - eslint-plugin-react-hooks: "npm:^5.0.0" + eslint-plugin-react-hooks: "npm:^7.0.0" eslint-plugin-styled-components-a11y: "npm:^2.1.35" eslint-plugin-testing-library: "npm:^7.0.0" jest: "npm:^29.7.0" From 5c80e626db1674195c1496776d7074527bedf4e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:16:24 +0000 Subject: [PATCH 2/6] chore(deps): update dependency eslint-plugin-react-hooks to v7 --- frontends/package.json | 2 +- yarn.lock | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/frontends/package.json b/frontends/package.json index 4824764354..d337b69ac9 100644 --- a/frontends/package.json +++ b/frontends/package.json @@ -54,7 +54,7 @@ "eslint-plugin-jest": "^29.0.0", "eslint-plugin-mdx": "^3.0.0", "eslint-plugin-react": "^7.34.3", - "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-styled-components-a11y": "^2.1.35", "eslint-plugin-testing-library": "^7.0.0", "jest": "^29.7.0", diff --git a/yarn.lock b/yarn.lock index 878ef9db4d..73fb87f7f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12090,15 +12090,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react-hooks@npm:^5.0.0": - version: 5.1.0 - resolution: "eslint-plugin-react-hooks@npm:5.1.0" - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - checksum: 10/b6778fd9e1940b06868921309e8b269426e17eda555816d4b71def4dcf0572de1199fdb627ac09ce42160b9569a93cd9b0fd81b740ab4df98205461c53997a43 - languageName: node - linkType: hard - "eslint-plugin-react-hooks@npm:^7.0.0": version: 7.0.1 resolution: "eslint-plugin-react-hooks@npm:7.0.1" @@ -12858,7 +12849,7 @@ __metadata: eslint-plugin-jest: "npm:^29.0.0" eslint-plugin-mdx: "npm:^3.0.0" eslint-plugin-react: "npm:^7.34.3" - eslint-plugin-react-hooks: "npm:^5.0.0" + eslint-plugin-react-hooks: "npm:^7.0.0" eslint-plugin-styled-components-a11y: "npm:^2.1.35" eslint-plugin-testing-library: "npm:^7.0.0" jest: "npm:^29.7.0" From 1329b974f52bd942e08c1420efad2ee60eedb241 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:01:19 +0100 Subject: [PATCH 3/6] React lint fixes --- .../CertificatePage/CertificatePage.tsx | 9 +++- .../ChannelPage/ChannelPageTemplate.tsx | 27 +++++------- .../HomePage/TestimonialsSection.tsx | 19 ++++---- frontends/main/src/app/page.tsx | 6 ++- .../ItemsListing/ItemsListing.tsx | 17 +++++-- .../LearningResourceExpanded.tsx | 25 +++++++---- .../ResourceDescription.tsx | 6 +-- .../ResourceCard/ResourceCard.tsx | 40 ++++++++--------- .../page-components/TiptapEditor/useSchema.ts | 44 +++++++++++++------ .../src/components/Dialog/Dialog.stories.tsx | 4 +- .../components/EmbedlyCard/EmbedlyCard.tsx | 14 +++--- .../NavDrawer/NavDrawer.stories.tsx | 10 ++--- .../ol-utilities/src/hooks/useThrottle.tsx | 7 +-- frontends/ol-utilities/src/ssr/NoSSR.tsx | 4 +- 14 files changed, 136 insertions(+), 96 deletions(-) diff --git a/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx index 7b46585ebf..3508e78d0d 100644 --- a/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx +++ b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx @@ -726,7 +726,12 @@ const CertificatePage: React.FC<{ }, [print]) const [shareOpen, setShareOpen] = useState(false) - const shareButtonRef = useRef(null) + const [shareAnchorEl, setShareAnchorEl] = useState( + null, + ) + const shareButtonRef = (node: HTMLDivElement | null) => { + setShareAnchorEl(node) + } if (isCourseLoading || isProgramLoading) { return @@ -776,7 +781,7 @@ const CertificatePage: React.FC<{ setShareOpen(false)} pageUrl={pageUrl} /> diff --git a/frontends/main/src/app-pages/ChannelPage/ChannelPageTemplate.tsx b/frontends/main/src/app-pages/ChannelPage/ChannelPageTemplate.tsx index 1c7c3d48b7..a4ddd05b03 100644 --- a/frontends/main/src/app-pages/ChannelPage/ChannelPageTemplate.tsx +++ b/frontends/main/src/app-pages/ChannelPage/ChannelPageTemplate.tsx @@ -72,23 +72,18 @@ const ChannelPageTemplate: React.FC = ({ channelType, name, }) => { - const getChannelTemplate = (channelType: string) => { - switch (channelType) { - case ChannelTypeEnum.Unit: - return UnitChannelTemplate - case ChannelTypeEnum.Topic: - return TopicChannelTemplate - default: - return DefaultChannelTemplate - } + switch (channelType) { + case ChannelTypeEnum.Unit: + return {children} + case ChannelTypeEnum.Topic: + return {children} + default: + return ( + + {children} + + ) } - const ChannelTemplate = getChannelTemplate(channelType) - - return ( - - {children} - - ) } export { diff --git a/frontends/main/src/app-pages/HomePage/TestimonialsSection.tsx b/frontends/main/src/app-pages/HomePage/TestimonialsSection.tsx index df64b67c20..7a426a879a 100644 --- a/frontends/main/src/app-pages/HomePage/TestimonialsSection.tsx +++ b/frontends/main/src/app-pages/HomePage/TestimonialsSection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react" +import React, { useMemo } from "react" import { shuffle } from "lodash" import { Container, @@ -11,7 +11,6 @@ import { } from "ol-components" import { ActionButton } from "@mitodl/smoot-design" import { useTestimonialList } from "api/hooks/testimonials" -import type { Attestation } from "api/v0" import { RiArrowRightLine, RiArrowLeftLine } from "@remixicon/react" import Slider from "react-slick" import AttestantBlock from "@/page-components/TestimonialDisplay/AttestantBlock" @@ -231,14 +230,12 @@ const TestimonialTruncateText = styled(TruncateText)({ const SlickCarousel = () => { const { data } = useTestimonialList({ position: 1 }) const [slick, setSlick] = React.useState(null) - const [shuffled, setShuffled] = useState() - const [imageSequence, setImageSequence] = useState() - - useEffect(() => { - if (!data) return - setShuffled(shuffle(data?.results)) - setImageSequence(shuffle([1, 2, 3, 4, 5, 6])) - }, [data]) + const results = data?.results + const shuffled = useMemo( + () => (results ? shuffle(results) : undefined), + [results], + ) + const imageSequence = useMemo(() => shuffle([1, 2, 3, 4, 5, 6]), []) if (!data?.results?.length || !shuffled?.length) { return null @@ -277,7 +274,7 @@ const SlickCarousel = () => { > Math.floor(Math.random() * 5) + 1 + export async function generateMetadata({ searchParams, }: PageProps<"/">): Promise { @@ -101,9 +103,11 @@ const Page: React.FC> = async () => { ), ]) + const heroImageIndex = getRandomHeroImageIndex() + return ( - + ) } diff --git a/frontends/main/src/page-components/ItemsListing/ItemsListing.tsx b/frontends/main/src/page-components/ItemsListing/ItemsListing.tsx index 42cf2ba161..593b2fab49 100644 --- a/frontends/main/src/page-components/ItemsListing/ItemsListing.tsx +++ b/frontends/main/src/page-components/ItemsListing/ItemsListing.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from "react" +import React, { useCallback, useEffect, useRef } from "react" import type { LearningResource } from "api" import { SortableItem, @@ -56,7 +56,10 @@ const ItemsListingSortable: React.FC<{ const moveLearningPathListItem = useLearningPathListItemMove() const moveUserListListItem = useUserListListItemMove() - const [sorted, setSorted] = React.useState([]) + const prevItemsRef = useRef(undefined) + const [sorted, setSorted] = React.useState( + () => items, + ) const ListCardComponent = condensed ? LearningResourceListCardCondensed @@ -69,7 +72,15 @@ const ItemsListingSortable: React.FC<{ * - `items` is the source of truth (most likely, this is coming from an API) * so sync `items` -> `sorted` when `items` changes. */ - useEffect(() => setSorted(items), [items]) + useEffect(() => { + if (prevItemsRef.current !== items) { + prevItemsRef.current = items + // Defer setState to avoid synchronous setState in effect + queueMicrotask(() => { + setSorted(items) + }) + } + }, [items]) const renderDragging: RenderActive = useCallback( (active) => { diff --git a/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx b/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx index 6d6721ec20..8cb6b14aa4 100644 --- a/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx +++ b/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx @@ -166,6 +166,7 @@ const LearningResourceExpanded: React.FC = ({ const titleSectionRef = useRef(null) const [titleSectionHeight, setTitleSectionHeight] = useState(0) const [scrollPosition, setScrollPosition] = useState(0) + const scrollElementRef = useRef(null) const [scrollElement, setScrollElement] = useState(null) const [chatExpanded, setChatExpanded] = useToggle(initialChatExpanded) @@ -173,12 +174,12 @@ const LearningResourceExpanded: React.FC = ({ if (outerContainerRef?.current?.scrollTo) { outerContainerRef.current.scrollTo(0, 0) } - if (scrollElement) { + if (scrollElementRef.current) { requestAnimationFrame(() => { - scrollElement.scrollTop = 0 + scrollElementRef.current!.scrollTop = 0 }) } - }, [resourceId, scrollElement]) + }, [resourceId]) useEffect(() => { const updateHeight = () => { @@ -202,20 +203,26 @@ const LearningResourceExpanded: React.FC = ({ const drawerPaper = outerContainerRef.current?.closest( ".MuiDrawer-paper", ) as HTMLElement - setScrollElement(drawerPaper) + scrollElementRef.current = drawerPaper + queueMicrotask(() => { + setScrollElement(drawerPaper) + }) } - }, [outerContainerRef]) + }, []) useEffect(() => { - if (scrollElement && chatTransitionState === ChatTransitionState.Closing) { - scrollElement.scrollTop = scrollPosition + if ( + scrollElementRef.current && + chatTransitionState === ChatTransitionState.Closing + ) { + scrollElementRef.current.scrollTop = scrollPosition } - }, [chatTransitionState, scrollElement, scrollPosition]) + }, [chatTransitionState, scrollPosition]) const onChatOpenerToggle = (open: boolean) => { if (open) { setChatTransitionState(ChatTransitionState.Opening) - setScrollPosition(scrollElement?.scrollTop ?? 0) + setScrollPosition(scrollElementRef.current?.scrollTop ?? 0) openChat() } else { setChatTransitionState(ChatTransitionState.Closing) diff --git a/frontends/main/src/page-components/LearningResourceExpanded/ResourceDescription.tsx b/frontends/main/src/page-components/LearningResourceExpanded/ResourceDescription.tsx index 56b69564dc..c26b4f339c 100644 --- a/frontends/main/src/page-components/LearningResourceExpanded/ResourceDescription.tsx +++ b/frontends/main/src/page-components/LearningResourceExpanded/ResourceDescription.tsx @@ -46,7 +46,7 @@ const DescriptionExpanded = styled(Description)({ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => { const firstRender = useRef(true) - const clampedOnFirstRender = useRef(false) + const [clampedOnFirstRender, setClampedOnFirstRender] = useState(false) const [isClamped, setClamped] = useState(false) const [isExpanded, setExpanded] = useState(false) const descriptionRendered = useCallback((node: HTMLDivElement) => { @@ -55,7 +55,7 @@ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => { setClamped(clamped) if (firstRender.current) { firstRender.current = false - clampedOnFirstRender.current = clamped + setClampedOnFirstRender(clamped) return } } @@ -87,7 +87,7 @@ const ResourceDescription = ({ resource }: { resource?: LearningResource }) => { dangerouslySetInnerHTML={{ __html: resource.description || "" }} lang={getResourceLanguage(resource)} /> - {(isClamped || clampedOnFirstRender.current) && ( + {(isClamped || clampedOnFirstRender) && ( { setAnchorEl(null) }, []) const handleAddToLearningPathClick: LearningResourceCardProps["onAddToLearningPathClick"] = - useMemo(() => { - if (user?.is_authenticated && user?.is_learning_path_editor) { - return (event, resourceId: number) => { + useCallback( + (event: React.MouseEvent, resourceId: number) => { + if (user?.is_authenticated && user?.is_learning_path_editor) { NiceModal.show(AddToLearningPathDialog, { resourceId }) } - } - return null - }, [user]) + }, + [user], + ) as LearningResourceCardProps["onAddToLearningPathClick"] const handleAddToUserListClick: LearningResourceCardProps["onAddToUserListClick"] = - useMemo(() => { - if (!user) { - // user info is still loading - return null - } - if (user.is_authenticated) { - return (event, resourceId: number) => { - NiceModal.show(AddToUserListDialog, { resourceId }) + useCallback( + (event: React.MouseEvent, resourceId?: number) => { + if (!user) { + // user info is still loading + return } - } - return (event) => { - setAnchorEl(event.currentTarget) - } - }, [user]) + if (user.is_authenticated) { + NiceModal.show(AddToUserListDialog, { resourceId: resourceId! }) + } else { + setAnchorEl(event.currentTarget as HTMLElement) + } + }, + [user], + ) const onClick = useLearningResourceDetailSetCache(resource) diff --git a/frontends/main/src/page-components/TiptapEditor/useSchema.ts b/frontends/main/src/page-components/TiptapEditor/useSchema.ts index 0bc15cb6e2..8fbf008ab0 100644 --- a/frontends/main/src/page-components/TiptapEditor/useSchema.ts +++ b/frontends/main/src/page-components/TiptapEditor/useSchema.ts @@ -25,7 +25,9 @@ export const useSchema = ({ const docType = schema.nodes.doc if (!docType) { - setSchemaError("Document node type not found in schema") + queueMicrotask(() => { + setSchemaError("Document node type not found in schema") + }) return } @@ -41,32 +43,40 @@ export const useSchema = ({ if (typeof nodeTypeName !== "string") { const errorMessage = "Invalid content for node doc: node type must be a string" - setSchemaError(`Document schema check failed: ${errorMessage}`) + queueMicrotask(() => { + setSchemaError(`Document schema check failed: ${errorMessage}`) + }) return } const nodeType = schema.nodes[nodeTypeName] if (!nodeType) { const errorMessage = `Invalid content for node doc: node type "${nodeTypeName}" not found in schema` - setSchemaError(`Document schema check failed: ${errorMessage}`) + queueMicrotask(() => { + setSchemaError(`Document schema check failed: ${errorMessage}`) + }) return } const nextMatch = match.matchType(nodeType) if (!nextMatch) { - setSchemaError( - `Document schema check failed: Invalid content for node doc: ${jsonNode.type} is not allowed in this position`, - ) + queueMicrotask(() => { + setSchemaError( + `Document schema check failed: Invalid content for node doc: ${jsonNode.type} is not allowed in this position`, + ) + }) return } match = nextMatch } if (!match.validEnd) { - setSchemaError( - "Document schema check failed: Invalid content for node doc: content specification not satisfied", - ) + queueMicrotask(() => { + setSchemaError( + "Document schema check failed: Invalid content for node doc: content specification not satisfied", + ) + }) return } @@ -74,18 +84,24 @@ export const useSchema = ({ try { contentNode = Node.fromJSON(schema, content) } catch (parseError) { - setSchemaError( - `Failed to parse content: ${parseError instanceof Error ? parseError.message : String(parseError)}`, - ) + queueMicrotask(() => { + setSchemaError( + `Failed to parse content: ${parseError instanceof Error ? parseError.message : String(parseError)}`, + ) + }) return } try { contentNode.check() - setSchemaError(null) + queueMicrotask(() => { + setSchemaError(null) + }) } catch (error) { const errorMessage = `Document schema check failed: ${error instanceof Error ? error.message : String(error)}` - setSchemaError(errorMessage) + queueMicrotask(() => { + setSchemaError(errorMessage) + }) } }, [schema, content, enabled]) diff --git a/frontends/ol-components/src/components/Dialog/Dialog.stories.tsx b/frontends/ol-components/src/components/Dialog/Dialog.stories.tsx index ae2343c9ac..c0d4ff3781 100644 --- a/frontends/ol-components/src/components/Dialog/Dialog.stories.tsx +++ b/frontends/ol-components/src/components/Dialog/Dialog.stories.tsx @@ -7,7 +7,9 @@ const StateWrapper = (props: DialogProps) => { const [open, setOpen] = useState(props.open) useEffect(() => { - setOpen(props.open) + queueMicrotask(() => { + setOpen(props.open) + }) }, [props.open]) const close = () => { diff --git a/frontends/ol-components/src/components/EmbedlyCard/EmbedlyCard.tsx b/frontends/ol-components/src/components/EmbedlyCard/EmbedlyCard.tsx index 6064544f4c..98143b14f7 100644 --- a/frontends/ol-components/src/components/EmbedlyCard/EmbedlyCard.tsx +++ b/frontends/ol-components/src/components/EmbedlyCard/EmbedlyCard.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react" +import React, { useCallback, useEffect, useRef } from "react" import styled from "@emotion/styled" import isURL from "validator/lib/isURL" import { @@ -54,12 +54,12 @@ const EmbedlyCard: React.FC = ({ url, aspectRatio, }) => { - const [container, setContainer] = useState(null) + const containerRef = useRef(null) const renderCard = useCallback((div: HTMLElement | null) => { if (!div) return div.addEventListener(EmbedlyEventTypes.CardCreated, insertCardStylesheet) - setContainer(div) + containerRef.current = div }, []) useEffect(() => { @@ -79,8 +79,8 @@ const EmbedlyCard: React.FC = ({ * to integrate with React: When the URL changes, we need to manually remove * the IFrame and insert a new anchor, which Embedly can then manipulate. */ - if (!container) return - container.innerHTML = "" + if (!containerRef.current) return + containerRef.current.innerHTML = "" if (!isURL(url)) return const a = document.createElement("a") a.dataset.cardChrome = "0" @@ -89,8 +89,8 @@ const EmbedlyCard: React.FC = ({ a.href = url a.classList.add("embedly-card") a.dataset["testid"] = "embedly-card" - container.appendChild(a) - }, [container, url]) + containerRef.current.appendChild(a) + }, [url]) return ( { const [open, setOpen] = useToggle(false) @@ -44,11 +49,6 @@ const NavDrawerDemo = () => { ], } - const StyledButton = styled(MuiButton)({ - position: "absolute", - right: "20px", - }) - return (
diff --git a/frontends/ol-utilities/src/hooks/useThrottle.tsx b/frontends/ol-utilities/src/hooks/useThrottle.tsx index 72f2de2437..8e1387105f 100644 --- a/frontends/ol-utilities/src/hooks/useThrottle.tsx +++ b/frontends/ol-utilities/src/hooks/useThrottle.tsx @@ -6,13 +6,14 @@ export const useThrottle = void>( callback: T, delay: number, ) => { - const lastRun = useRef(Date.now()) + const lastRun = useRef(null) return useCallback( (...args: Parameters) => { - if (Date.now() - lastRun.current >= delay) { + const now = Date.now() + if (lastRun.current === null || now - lastRun.current >= delay) { callback(...args) - lastRun.current = Date.now() + lastRun.current = now } }, [callback, delay], diff --git a/frontends/ol-utilities/src/ssr/NoSSR.tsx b/frontends/ol-utilities/src/ssr/NoSSR.tsx index a6aa80d660..3e5ae748f1 100644 --- a/frontends/ol-utilities/src/ssr/NoSSR.tsx +++ b/frontends/ol-utilities/src/ssr/NoSSR.tsx @@ -11,7 +11,9 @@ export const NoSSR: React.FC = ({ children, onSSR = null }) => { const [isClient, setClient] = useState(false) useEffect(() => { - setClient(true) + queueMicrotask(() => { + setClient(true) + }) }, []) return isClient ? children : onSSR From 39950614df8153466db95615db077f33f48591e5 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:36:27 +0100 Subject: [PATCH 4/6] Test fixes --- .../ResourceCard/ResourceCard.test.tsx | 33 +++++++++++-------- .../ResourceCard/ResourceCard.tsx | 23 ++++++++----- frontends/ol-utilities/src/ssr/NoSSR.tsx | 4 +-- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/frontends/main/src/page-components/ResourceCard/ResourceCard.test.tsx b/frontends/main/src/page-components/ResourceCard/ResourceCard.test.tsx index da1ec6f8e1..022d791ffc 100644 --- a/frontends/main/src/page-components/ResourceCard/ResourceCard.test.tsx +++ b/frontends/main/src/page-components/ResourceCard/ResourceCard.test.tsx @@ -12,10 +12,6 @@ import { learningResourceQueries } from "api/hooks/learningResources" import { ResourceCard } from "./ResourceCard" import { getReadableResourceType } from "ol-utilities" import { ResourceTypeEnum, MicroUserListRelationship } from "api" -import { - AddToLearningPathDialog, - AddToUserListDialog, -} from "../Dialogs/AddToListDialog" import type { ResourceCardProps } from "./ResourceCard" import { urls, factories, setMockResponse } from "api/test-utils" import { RESOURCE_DRAWER_PARAMS } from "@/common/urls" @@ -112,9 +108,13 @@ describe.each([ name: `Bookmark ${getReadableResourceType(resource?.resource_type as ResourceTypeEnum)}`, }) - const addToLearningPathButton = screen.queryByRole("button", { - name: "Add to Learning Path", - }) + const addToLearningPathButton = expectAddToLearningPathButton + ? await screen.findByRole("button", { + name: "Add to Learning Path", + }) + : screen.queryByRole("button", { + name: "Add to Learning Path", + }) expect(!!addToLearningPathButton).toBe(expectAddToLearningPathButton) }, ) @@ -189,13 +189,20 @@ describe.each([ expect(showModal).not.toHaveBeenCalled() await user.click(addToLearningPathButton) invariant(resource) - expect(showModal).toHaveBeenLastCalledWith(AddToLearningPathDialog, { - resourceId: resource.id, - }) + expect(showModal).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ + resourceId: resource.id, + }), + ) + showModal.mockClear() await user.click(addToUserListButton) - expect(showModal).toHaveBeenLastCalledWith(AddToUserListDialog, { - resourceId: resource.id, - }) + expect(showModal).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ + resourceId: resource.id, + }), + ) }) test("Clicking 'Add to User List' opens signup popover if not authenticated", async () => { diff --git a/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx b/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx index ed1eb41256..e1a6d2ad33 100644 --- a/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx +++ b/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react" +import React, { useCallback, useState, useMemo } from "react" import { LearningResourceCard, LearningResourceListCard, @@ -29,15 +29,20 @@ export const useResourceCard = (resource?: LearningResource | null) => { const handleClosePopover = useCallback(() => { setAnchorEl(null) }, []) + const handleAddToLearningPathClickCallback = useCallback( + (event: React.MouseEvent, resourceId: number) => { + NiceModal.show(AddToLearningPathDialog, { resourceId }) + }, + [], + ) const handleAddToLearningPathClick: LearningResourceCardProps["onAddToLearningPathClick"] = - useCallback( - (event: React.MouseEvent, resourceId: number) => { - if (user?.is_authenticated && user?.is_learning_path_editor) { - NiceModal.show(AddToLearningPathDialog, { resourceId }) - } - }, - [user], - ) as LearningResourceCardProps["onAddToLearningPathClick"] + useMemo( + () => + user?.is_authenticated && user?.is_learning_path_editor + ? handleAddToLearningPathClickCallback + : null, + [user, handleAddToLearningPathClickCallback], + ) const handleAddToUserListClick: LearningResourceCardProps["onAddToUserListClick"] = useCallback( diff --git a/frontends/ol-utilities/src/ssr/NoSSR.tsx b/frontends/ol-utilities/src/ssr/NoSSR.tsx index 3e5ae748f1..21f45bff4f 100644 --- a/frontends/ol-utilities/src/ssr/NoSSR.tsx +++ b/frontends/ol-utilities/src/ssr/NoSSR.tsx @@ -1,6 +1,6 @@ "use client" -import React, { useState, useEffect, ReactNode } from "react" +import React, { useState, useEffect, ReactNode, startTransition } from "react" type NoSSRProps = { children: ReactNode @@ -11,7 +11,7 @@ export const NoSSR: React.FC = ({ children, onSSR = null }) => { const [isClient, setClient] = useState(false) useEffect(() => { - queueMicrotask(() => { + startTransition(() => { setClient(true) }) }, []) From 798e5fa58fa7ffa5fefc622e10a9b3c3fc1e205d Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Fri, 20 Feb 2026 13:52:36 -0500 Subject: [PATCH 5/6] fix for strict syntax --- .../node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx b/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx index d7ef06f890..25c017a37b 100644 --- a/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx +++ b/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx @@ -100,6 +100,7 @@ export const ArticleByLineInfoBarContent = ({ authorName, onAuthorNameChange, }: ArticleByLineInfoBarContentProps) => { + const [anchorEl, setAnchorEl] = useState(null) const [shareOpen, setShareOpen] = useState(false) const shareButtonRef = useRef(null) @@ -115,7 +116,7 @@ export const ArticleByLineInfoBarContent = ({ setShareOpen(false)} pageUrl={`${NEXT_PUBLIC_ORIGIN}/articles/${article?.slug}`} /> @@ -155,7 +156,10 @@ export const ArticleByLineInfoBarContent = ({ variant="bordered" edge="circular" aria-label="Share this article" - onClick={() => setShareOpen(true)} + onClick={(e) => { + setAnchorEl(e.currentTarget) + setShareOpen(true) + }} > From 13e3158063cb12a0a88d07188a125727acfe7821 Mon Sep 17 00:00:00 2001 From: shankar ambady Date: Fri, 20 Feb 2026 14:01:20 -0500 Subject: [PATCH 6/6] Revert "fix for strict syntax" This reverts commit 798e5fa58fa7ffa5fefc622e10a9b3c3fc1e205d. --- .../node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx b/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx index 25c017a37b..d7ef06f890 100644 --- a/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx +++ b/frontends/main/src/page-components/TiptapEditor/extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBar.tsx @@ -100,7 +100,6 @@ export const ArticleByLineInfoBarContent = ({ authorName, onAuthorNameChange, }: ArticleByLineInfoBarContentProps) => { - const [anchorEl, setAnchorEl] = useState(null) const [shareOpen, setShareOpen] = useState(false) const shareButtonRef = useRef(null) @@ -116,7 +115,7 @@ export const ArticleByLineInfoBarContent = ({ setShareOpen(false)} pageUrl={`${NEXT_PUBLIC_ORIGIN}/articles/${article?.slug}`} /> @@ -156,10 +155,7 @@ export const ArticleByLineInfoBarContent = ({ variant="bordered" edge="circular" aria-label="Share this article" - onClick={(e) => { - setAnchorEl(e.currentTarget) - setShareOpen(true) - }} + onClick={() => setShareOpen(true)} >