I’m trying to create a description box that can be edited in "edit mode", but just diplays the static definition otherwise.
During edit mode, I want the state to be recorded in the react state "editedHTMLDefinition", before being recorded to the global state when exiting "edit mode".
const EditableDescription = ({
htmlDefinition,
setHTMLDefinition = () => {},
editMode,
word,
}) => {
const [editorState, setEditorState] = useState();
const [editedHTMLDefinition, setEditedHTMLDefinition] = useState(null);
const editedHTMLRef = useRef(null);
// const editorStateRef = useRef(null);
const onChange = (newEditorState, editor) => {
setEditorState(newEditorState);
editor.current = newEditorState;
};
const CustomContent = (
<ContentEditable
style={{
position: "relative",
borderColor: "rgba(255,211,2,0.68)",
border: "1px solid gray",
borderRadius: "5px",
maxWidth: "100%",
padding: "10px",
}}
value={editorState} // Use value prop to set the content
onChange={onChange} // Handle content changes
/>
);
function LoadContentPlugin() {
const [editor] = useLexicalComposerContext();
editor.update(() => {
const parser = new DOMParser();
const dom = parser.parseFromString(htmlDefinition, "text/html");
// Once you have the DOM instance it's easy to generate LexicalNodes.
const nodes = $generateNodesFromDOM(editor, dom);
// Select the root
const root = $getRoot();
const paragraphNode = $createParagraphNode();
nodes.forEach((n) => paragraphNode.append(n));
root.append(paragraphNode);
});
}
const lexicalConfig = {
namespace: word,
onError: (e) => {
console.log("ERROR:", e);
},
};
const editableDescription = (
<div style={{ padding: "20px" }}>
<LexicalComposer initialConfig={lexicalConfig}>
<RichTextPlugin
contentEditable={CustomContent}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin
onChange={(editorState, editor) => {
editor.update(() => {
setEditedHTMLDefinition((prevEditedHTMLDefinition) => {
const newEditedHTMLDefinition = String(
$generateHtmlFromNodes(editor, null)
);
console.log(
"EDITOR STATE: ",
JSON.stringify(editorState),
"nHTML: ",
$generateHtmlFromNodes(editor, null),
"nEdited HTML: ",
newEditedHTMLDefinition,
"nprevEditedHTMLDefinition",
prevEditedHTMLDefinition
);
return newEditedHTMLDefinition;
});
editedHTMLRef.current = $generateHtmlFromNodes(editor, null);
});
}}
/>
<HistoryPlugin />
<LoadContentPlugin />
</LexicalComposer>
</div>
);
const [descriptionBox, setDescriptionBox] = useState(
<StaticDescription htmlDefinition={htmlDefinition} />
);
useEffect(() => {
console.log("editedHTMLDefinition changed: ", editedHTMLDefinition);
}, [editedHTMLDefinition]);
useEffect(() => {
console.log("Edit mode changed for ", htmlDefinition);
if (!editMode) {
// handle save
console.log("internal edited desc: ", editedHTMLDefinition);
console.log("ref state: ", editedHTMLRef);
setHTMLDefinition(editedHTMLDefinition);
setDescriptionBox(<StaticDescription htmlDefinition={htmlDefinition} />);
} else {
// switch to edit mode
setDescriptionBox(editableDescription);
}
}, [editMode]);
return descriptionBox;
};
The issue is, the state is not being recorded as expected. In the first "useEffect" call, as I’m editing, I’m getting the correct values from the state based on what I’ve written in the box.
However, when I change the editMode, the editedHTMLDefinition gives me "null" as the value (same as its initial state). I tried using a ref instead but I got the same result.
I feel like it might be because editMode is a react state thats being passed in, so maybe the component is rerendering when it changes, so the editedHTMLDefinition also resets. How do I solve this? I want the component to react to when editMode changes
editMode is a higher level state controlled by a single button that’s being passed to multiple editableDescriptions at once
Edit: The component where this is being used:
function Flashcards() {
const { collectionName } = useParams();
// Set a default value of "default_collection" if collectionName is not provided
const selectedCollection = collectionName
? collectionName
: "default_collection";
const [flashcards, setFlashcards] = useState(
localStorage.getItem("flashcards")
? JSON.parse(localStorage.getItem("flashcards"))
: []
);
const [editMode, setEditMode] = useState(false);
const toggleEditMode = () => {
setEditMode(!editMode);
console.log("Edited edit mode");
};
function Flashcard({ index, word, definition }) {
const wordField = (
<TextField
id="outlined-multiline-flexible"
multiline
style={{
flex: "1",
padding: "10px",
}}
value={word}
disabled={!editMode}
/>
);
return (
<>
<Box
key={index}
sx={{
boxShadow: "0px 10px 25px rgba(0, 0, 0, 0.05)",
backgroundColor: "#fff",
p: 2,
borderRadius: 2,
mt: 3,
position: "relative",
display: "flex",
}}
>
{wordField}
<EditableDescription
htmlDefinition={definition}
setHTMLDefinition={(newDef) => {
console.log("NEW DEFINITION", newDef);
}}
editMode={editMode}
/>
</Box>
</>
);
}
return (
<>
<Stack
sx={{ mb: 2 }}
direction="row"
alignItems="center"
justifyContent="space-between"
>
<IconButton to="/" component={Link} sx={{ color: "black", mr: 1 }}>
<BackIcon />
</IconButton>
<Typography variant="h6">Flashcards</Typography>
<Button onClick={toggleEditMode} color={editMode ? "success" :"error"}>
Edit
</Button>
</Stack>
{!!Object.keys(flashcards[selectedCollection]).length ? (
<Grid container spacing={2}>
{Object.keys(flashcards[selectedCollection]).map((index) => {
const wordInfo = flashcards[selectedCollection][index];
console.log("Index: ", index, " Wordifo: ", wordInfo);
const word = wordInfo.word;
const definition = wordInfo.definition;
return (
<Grid item xs={12} key={index}>
<Flashcard
index={index}
word={word}
definition={definition}
/>
</Grid>
);
})}
</Grid>
) : (
<Typography sx={{ mt: 5 }} align="center">
No Flashcards
</Typography>
)}
</>
);
}
export default Flashcards;
2
Answers
Always try to reduce the uses of
useState
withuseEffect
if possible.Here, rather than using
useEffect
witheditMode
, takeeditMode
from the props and use it as conditional rendering like below –So, based on
editMode
, it will render the JSX conditionally.Also, can you add the Component too?