Skip to content

Commit 313dc8f

Browse files
committed
Give IsaacContent object unique React keys
1 parent 4afc7c1 commit 313dc8f

File tree

1 file changed

+30
-19
lines changed

1 file changed

+30
-19
lines changed

src/app/components/content/IsaacContent.tsx

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,26 @@ export interface IsaacContentProps extends RouteComponentProps {
4242

4343
export const IsaacContent = withRouter((props: IsaacContentProps) => {
4444
const {doc: {type, layout, encoding, value, children}, match} = props;
45+
const keyedProps = {...props, key: props.doc.id};
46+
{/*
47+
Each IsaacContent is assumed to be independent, not sharing state with any other.
48+
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.
49+
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
50+
initialise as expected, but will retain stale data from A.
51+
52+
This is a problem for any structure where one of several <IsaacContent>s are displayed, e.g. quiz sections, tabs, ... .
53+
54+
To avoid this, we set the key of each <IsaacContent> to its content ID.
55+
*/}
4556

4657
let selectedComponent;
4758
let tempSelectedComponent;
4859
if (isQuestion(props.doc)) {
4960
// FIXME: Someday someone will remove /quiz/ and this comment too.
5061
if (match.path.startsWith("/quiz/") || match.path.startsWith("/test/")) {
51-
tempSelectedComponent = <QuizQuestion {...props} />;
62+
tempSelectedComponent = <QuizQuestion {...keyedProps} />;
5263
} else {
53-
tempSelectedComponent = <IsaacQuestion {...props} />;
64+
tempSelectedComponent = <IsaacQuestion {...keyedProps} />;
5465
}
5566

5667
if (type === "isaacInlineRegion") {
@@ -60,25 +71,25 @@ export const IsaacContent = withRouter((props: IsaacContentProps) => {
6071
selectedComponent = <QuestionContext.Provider value={props.doc.id}>{tempSelectedComponent}</QuestionContext.Provider>;
6172
} else {
6273
switch (type) {
63-
case "figure": selectedComponent = <IsaacFigure {...props} />; break;
64-
case "image": selectedComponent = <IsaacImage {...props} />; break;
65-
case "video": selectedComponent = <IsaacVideo {...props} />; break;
66-
case "codeSnippet": selectedComponent = <IsaacCodeSnippet {...props} />; break;
67-
case "interactiveCodeSnippet": selectedComponent = <IsaacInteractiveCodeSnippet {...props} />; break;
68-
case "glossaryTerm": selectedComponent = <IsaacGlossaryTerm {...props} />; break;
69-
case "isaacFeaturedProfile": selectedComponent = <IsaacFeaturedProfile {...props} />; break;
70-
case "isaacQuestion": selectedComponent = <IsaacQuickQuestion {...props} />; break;
71-
case "anvilApp": selectedComponent = <AnvilApp {...props} />; break;
72-
case "isaacCard": selectedComponent = <IsaacCard {...props} />; break;
73-
case "isaacCardDeck": selectedComponent = <IsaacCardDeck {...props} />; break;
74-
case "codeTabs": selectedComponent = <IsaacCodeTabs {...props} />; break;
74+
case "figure": selectedComponent = <IsaacFigure {...keyedProps} />; break;
75+
case "image": selectedComponent = <IsaacImage {...keyedProps} />; break;
76+
case "video": selectedComponent = <IsaacVideo {...keyedProps} />; break;
77+
case "codeSnippet": selectedComponent = <IsaacCodeSnippet {...keyedProps} />; break;
78+
case "interactiveCodeSnippet": selectedComponent = <IsaacInteractiveCodeSnippet {...keyedProps} />; break;
79+
case "glossaryTerm": selectedComponent = <IsaacGlossaryTerm {...keyedProps} />; break;
80+
case "isaacFeaturedProfile": selectedComponent = <IsaacFeaturedProfile {...keyedProps} />; break;
81+
case "isaacQuestion": selectedComponent = <IsaacQuickQuestion {...keyedProps} />; break;
82+
case "anvilApp": selectedComponent = <AnvilApp {...keyedProps} />; break;
83+
case "isaacCard": selectedComponent = <IsaacCard {...keyedProps} />; break;
84+
case "isaacCardDeck": selectedComponent = <IsaacCardDeck {...keyedProps} />; break;
85+
case "codeTabs": selectedComponent = <IsaacCodeTabs {...keyedProps} />; break;
7586
default:
7687
switch (layout) {
77-
case "tabs": selectedComponent = <IsaacTabs {...props} />; break;
78-
case isTabs(layout): selectedComponent = <IsaacTabs {...props} style={layout?.split('/')[1]} />; break;
79-
case "callout": selectedComponent = <IsaacCallout {...props} />; break;
80-
case "accordion": selectedComponent = <IsaacAccordion {...props} />; break;
81-
case "horizontal": selectedComponent = <IsaacHorizontal {...props} />; break;
88+
case "tabs": selectedComponent = <IsaacTabs {...keyedProps} />; break;
89+
case isTabs(layout): selectedComponent = <IsaacTabs {...keyedProps} style={layout?.split('/')[1]} />; break;
90+
case "callout": selectedComponent = <IsaacCallout {...keyedProps} />; break;
91+
case "accordion": selectedComponent = <IsaacAccordion {...keyedProps} />; break;
92+
case "horizontal": selectedComponent = <IsaacHorizontal {...keyedProps} />; break;
8293
case "clearfix": selectedComponent = <>&nbsp;</>; break;
8394
default: selectedComponent =
8495
<IsaacContentValueOrChildren encoding={encoding} value={value}>

0 commit comments

Comments
 (0)