skip to Main Content

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


  1. const EditableDescription = ({
      htmlDefinition,
      setHTMLDefinition = () => {},
      editMode,
      word,
    }) => {
      const [editorState, setEditorState] = useState();
      const [editedHTMLDefinition, setEditedHTMLDefinition] = useState(null);
      const editedHTMLRef = useRef(null);
      const [descriptionBox, setDescriptionBox] = useState(
        <StaticDescription htmlDefinition={htmlDefinition} />
      );
    
      useEffect(() => {
        // This effect will run when editMode changes
        if (editMode) {
          // If entering edit mode, set the editable description
          setDescriptionBox(
            <div style={{ padding: "20px" }}>
              {/* Your LexicalComposer and plugins here */}
            </div>
          );
        } else {
          // If exiting edit mode, save the edited HTML definition
          setHTMLDefinition(editedHTMLRef.current);
          setDescriptionBox(<StaticDescription htmlDefinition={htmlDefinition} />);
        }
      }, [editMode]);
    
      const onChange = (newEditorState, editor) => {
        setEditorState(newEditorState);
        editor.current = newEditorState;
    
        setEditedHTMLDefinition($generateHtmlFromNodes(editor, null));
        editedHTMLRef.current = $generateHtmlFromNodes(editor, null);
      };
    
      return (
        <div>
          {descriptionBox}
          <ContentEditable
            style={{
              position: "relative",
              borderColor: "rgba(255,211,2,0.68)",
              border: "1px solid gray",
              borderRadius: "5px",
              maxWidth: "100%",
              padding: "10px",
            }}
            value={editorState}
            onChange={onChange}
            disabled={!editMode} // Disable the editor when not in edit mode
          />
        </div>
      );
    };
    
    export default EditableDescription;
    
    Login or Signup to reply.
  2. Always try to reduce the uses of useState with useEffect if possible.

    Here, rather than using useEffect with editMode, take editMode from the props and use it as conditional rendering like below –

        return (
            editMode
                ? <div style={{ padding: "20px" }}>
                    <LexicalComposer initialConfig={lexicalConfig}>
                    <RichTextPlugin
                        contentEditable={CustomContent}
                        ErrorBoundary={LexicalErrorBoundary}
                    />
                    <OnChangePlugin ... />
                    <HistoryPlugin />
                    <LoadContentPlugin />
                    </LexicalComposer>
                </div>
                : <StaticDescription htmlDefinition={htmlDefinition} />
        )
    

    So, based on editMode, it will render the JSX conditionally.

    Also, can you add the Component too?

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search