From 313dc8f782c12405394138e04340f954c8d2be9e Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 22 May 2025 15:03:07 +0100 Subject: [PATCH] Give `IsaacContent` object unique React `key`s --- src/app/components/content/IsaacContent.tsx | 49 +++++++++++++-------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/app/components/content/IsaacContent.tsx b/src/app/components/content/IsaacContent.tsx index 14ef2bffa7..34106fbf2c 100644 --- a/src/app/components/content/IsaacContent.tsx +++ b/src/app/components/content/IsaacContent.tsx @@ -42,15 +42,26 @@ export interface IsaacContentProps extends RouteComponentProps { export const IsaacContent = withRouter((props: IsaacContentProps) => { const {doc: {type, layout, encoding, value, children}, match} = props; + const keyedProps = {...props, key: props.doc.id}; + {/* + Each IsaacContent is assumed to be independent, not sharing state with any other. + However, React will reuse components if they are the same type, have the same key (or undefined), and exist in the same place in the DOM. + If two components A and B meet these criteria, if you switch from component A to component B, any e.g. useStates in B will not + initialise as expected, but will retain stale data from A. + + This is a problem for any structure where one of several s are displayed, e.g. quiz sections, tabs, ... . + + To avoid this, we set the key of each to its content ID. + */} let selectedComponent; let tempSelectedComponent; if (isQuestion(props.doc)) { // FIXME: Someday someone will remove /quiz/ and this comment too. if (match.path.startsWith("/quiz/") || match.path.startsWith("/test/")) { - tempSelectedComponent = ; + tempSelectedComponent = ; } else { - tempSelectedComponent = ; + tempSelectedComponent = ; } if (type === "isaacInlineRegion") { @@ -60,25 +71,25 @@ export const IsaacContent = withRouter((props: IsaacContentProps) => { selectedComponent = {tempSelectedComponent}; } else { switch (type) { - case "figure": selectedComponent = ; break; - case "image": selectedComponent = ; break; - case "video": selectedComponent = ; break; - case "codeSnippet": selectedComponent = ; break; - case "interactiveCodeSnippet": selectedComponent = ; break; - case "glossaryTerm": selectedComponent = ; break; - case "isaacFeaturedProfile": selectedComponent = ; break; - case "isaacQuestion": selectedComponent = ; break; - case "anvilApp": selectedComponent = ; break; - case "isaacCard": selectedComponent = ; break; - case "isaacCardDeck": selectedComponent = ; break; - case "codeTabs": selectedComponent = ; break; + case "figure": selectedComponent = ; break; + case "image": selectedComponent = ; break; + case "video": selectedComponent = ; break; + case "codeSnippet": selectedComponent = ; break; + case "interactiveCodeSnippet": selectedComponent = ; break; + case "glossaryTerm": selectedComponent = ; break; + case "isaacFeaturedProfile": selectedComponent = ; break; + case "isaacQuestion": selectedComponent = ; break; + case "anvilApp": selectedComponent = ; break; + case "isaacCard": selectedComponent = ; break; + case "isaacCardDeck": selectedComponent = ; break; + case "codeTabs": selectedComponent = ; break; default: switch (layout) { - case "tabs": selectedComponent = ; break; - case isTabs(layout): selectedComponent = ; break; - case "callout": selectedComponent = ; break; - case "accordion": selectedComponent = ; break; - case "horizontal": selectedComponent = ; break; + case "tabs": selectedComponent = ; break; + case isTabs(layout): selectedComponent = ; break; + case "callout": selectedComponent = ; break; + case "accordion": selectedComponent = ; break; + case "horizontal": selectedComponent = ; break; case "clearfix": selectedComponent = <> ; break; default: selectedComponent =