1

so I have 2 components.

  1. The TextEditor by lexical
  2. Insight component which uses the TextEditor and adds some functionality The TextEditor code:
import ExampleTheme from "./themes/ExampleTheme";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import ToolbarPlugin from "./plugins/ToolbarPlugin";
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";
import { ListItemNode, ListNode } from "@lexical/list";
import { CodeHighlightNode, CodeNode } from "@lexical/code";
import { AutoLinkNode, LinkNode } from "@lexical/link";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { TRANSFORMERS } from "@lexical/markdown";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin";
import AutoLinkPlugin from "./plugins/AutoLinkPlugin";
import "./TextEditor.css";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useEffect } from "react";

function Placeholder() {
  return <div className="editor-placeholder">כתוב את התובנה כאן:</div>;
}

const editorConfig = {
  // The editor theme
  theme: ExampleTheme,
  // Handling of errors during update
  onError(error) {
    throw error;
  },
  // Any custom nodes go here
  nodes: [
    HeadingNode,
    ListNode,
    ListItemNode,
    QuoteNode,
    CodeNode,
    CodeHighlightNode,
    TableNode,
    TableCellNode,
    TableRowNode,
    AutoLinkNode,
    LinkNode,
  ],
};

const emptyEditor =
  '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
function TextEditorEditable({ editable = false }) {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    // defines whether the editor is on edit or read mode
    editor.setEditable(editable);
  }, [editor, editable]);

  return null;
}

function TextEditorInitialValue({ value = null }) {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    // defines whether the editor is on edit or read mode
    if (value === null) return;

    const initialEditorState = editor.parseEditorState(
      value === "" ? emptyEditor : value
    );
    editor.setEditorState(initialEditorState);
  }, [editor, value]);

  return null;
}

export default function TextEditor({ editMode, onChange, initialValue }) {
  if (initialValue) {
    editorConfig["editorState"] = initialValue;
  }
  editorConfig["editable"] = editMode;
  return (
    <LexicalComposer initialConfig={editorConfig}>
      <div className="editor-container">
        {editMode && <ToolbarPlugin />}
        <div className="editor-inner">
          <RichTextPlugin
            contentEditable={<ContentEditable className="editor-input" />}
            placeholder={<Placeholder />}
            ErrorBoundary={LexicalErrorBoundary}
            editorState={initialValue}
          />
          <OnChangePlugin onChange={onChange} />
          <HistoryPlugin />
          <AutoFocusPlugin />
          <CodeHighlightPlugin />
          <ListPlugin />
          <LinkPlugin />
          <AutoLinkPlugin />
          <ListMaxIndentLevelPlugin maxDepth={7} />
          <MarkdownShortcutPlugin transformers={TRANSFORMERS} />
        </div>
      </div>
      <TextEditorEditable editable={editMode} />
      <TextEditorInitialValue value={initialValue} />
    </LexicalComposer>
  );
}

The Insight code:

import react, { useCallback, useEffect, useMemo } from "react";
import { useAssessmentContext } from "../contexts/AssessmentContext";
import TextEditor from "./TextEditor";
import { useFetchInsightsQuery } from "../store";

function assignInsights(insights, apiInsights) {
  const emptyInsights = Object.keys(insights).reduce((obj, key) => {
    obj[key] = "";
    return obj;
  }, {});
  return Object.assign(emptyInsights, apiInsights);
}

function useApiData() {
  const { activeAssessment, setInsights } = useAssessmentContext();
  const {
    data: assessmentInsights,
    // error: insightsError,
    // isLoading: insightsIsLoading,
  } = useFetchInsightsQuery(activeAssessment?.id, {
    skip: activeAssessment?.id ? false : true,
  });
  useEffect(() => {
    if (!assessmentInsights) return;
    const apiInsights = assessmentInsights.reduce(
      (newInsightObject, insight) => {
        newInsightObject[insight.insight_name] = insight.info;
        return newInsightObject;
      },
      {}
    );
    setInsights((insights) => assignInsights(insights, apiInsights));
  }, [assessmentInsights, setInsights, activeAssessment]);
}

function Insight({ insightName }) {
  const { insights, setInsights, editMode, activeAssessment } =
    useAssessmentContext();
  useApiData();
  // Get from api
  const currentSavedInsightText = insights[insightName];
  // The value is of type editorState
  const onChange = useCallback(
    (value) => {
      const newValue = JSON.stringify(value);
      const oldValue = insights[insightName];
      if (newValue === oldValue) return;
      setInsights((insights) => ({ ...insights, [insightName]: newValue }));
    },
    [insightName, setInsights] // eslint-disable-line react-hooks/exhaustive-deps
  ); 
  // Only re-reender the component if the insight saved value changes
  const memoizedTextEditor = useMemo(() => {
    return (
      <TextEditor
        onChange={onChange}
        editMode={editMode}
        initialValue={currentSavedInsightText}
      />
    );
  }, [editMode, onChange, activeAssessment, currentSavedInsightText]); // eslint-disable-line react-hooks/exhaustive-deps
  return memoizedTextEditor;
}

export default react.memo(Insight);

The vital part to understanding the question is this:

insight is an object with multiple keys insightName: value I have this variable:

const currentSavedInsightText = insights[insightName];

Which sets the initial value for the TextEditor.

Now the problem starts with me having 2 Insight of the same kind (same insightName) One on the screen one that opens as a popup (expand on the whole screen easier to write) meaning I need to synchronize the two.

Now The only way I found possible to do this is by adding the currentSavedInsightText to the useMemo dependencies. Still, then the TextEditor loses focus and only lets me write a single letter at a time.

What can I do for the TextEditor to either not lose focus or use a state for its value as you do with a normal input where you connect the value to always show a state and change that state on keystroke

Matan
  • 73
  • 2
  • 14
  • I don't think you should be using `initialValue` if you need to sync two inputs. It makes your input a controlled input, so instead you should be using a `value`. – Robo Robok Apr 12 '23 at 16:32

0 Answers0