contentful / field-editors

React components and extensions for building Contentful entry editor
https://contentful-field-editors.netlify.app/
MIT License
150 stars 118 forks source link

@contentful/field-editor-rich-text re-rendering problem #1173

Open giovanni-caiazzo opened 2 years ago

giovanni-caiazzo commented 2 years ago

Hi all, I am using @contentful/field-editor-rich-text in a contentful app, version 2.1.0 and I have some problems with rerendering. Basically I am rendering a set of tabs and each tab has an instance of @contentful/field-editor-rich-text:

<ConnectedRichTextEditor
sdk={sdk}
value={accordion.content}
onChange={(e: any) =>
setGlobalData((prevState) => {
    const newState = [...prevState];
    newState[idx].content = e;
    return newState;
})}
minHeight={"300px"}
/>

When the component first renders it works great, but as soon as I change tab, the editor disappears and never reappears even when I return to the original tab. When the page is reloaded the editor reappears, only to disappear again as soon as a new tab is selected. I also tried to render all the editors together without tabs: what happens is that all the editors have the content of the last rendered editor, which is not ideal.

Here is the full Field component

const Field = () => {
  const sdk = useSDK<FieldExtensionSDK>();
  const [globalData, setGlobalData] = useState<AccordionData[]>(
    sdk.field.getValue() || [{ title: "", content: emptyRTF }]
  );
  const [currentTab, setCurrentTab] = useState("accordion-0");

  useEffect(() => {
    sdk.window.startAutoResizer();
  });

  useEffect(() => {
    if (globalData && sdk.field.getValue() !== globalData) {
      sdk.field.setValue(globalData).then();
    }
  }, [globalData, sdk.field]);

  return (
    <>
      <Note style={{ marginTop: "10px", marginBottom: "20px" }}>
        You can create more than one accordion in this component. Use "Add
        Accordion" to create one. If you want to delete a particular accordion
        you can use the "Delete Accordion" button of the relative Accordion.
      </Note>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        <Button
          style={{ marginBottom: "20px" }}
          variant="primary"
          onClick={() => {
            setGlobalData((prevState) => [
              ...prevState,
              { title: "", content: emptyRTF },
            ]);
            const currentIdx = parseInt(currentTab.split("-")[1]);
            setCurrentTab(`accordion-${currentIdx + 1}`);
          }}
        >
          Add Accordion
        </Button>
      </div>
      <Tabs currentTab={currentTab} onTabChange={setCurrentTab}>
        <Tabs.List>
          {globalData.map((accordion, idx) => {
            return (
              <Tabs.Tab panelId={`accordion-${idx}`} key={`accordion-${idx}`}>
                A. # {idx + 1}
              </Tabs.Tab>
            );
          })}
        </Tabs.List>

        {globalData.map((accordion, idx) => {
          return (
            <Tabs.Panel id={`accordion-${idx}`} key={`accordion-${idx}`}>
              <h2 style={{ marginTop: "20px", marginBottom: "10px" }}>Title</h2>
              <TextInput
                value={accordion.title}
                onChange={(e: any) =>
                  setGlobalData((prevState) => {
                    const newState = [...prevState];
                    newState[idx].title = e.target.value;
                    return newState;
                  })
                }
                title={"Header"}
                placeholder={"Header"}
              />
              <h2 style={{ marginTop: "30px", marginBottom: "10px" }}>
                Content
              </h2>
              <ConnectedRichTextEditor
                // @ts-ignore
                sdk={sdk}
                value={accordion.content}
                onChange={(e: any) =>
                  setGlobalData((prevState) => {
                    const newState = [...prevState];
                    newState[idx].content = e;
                    return newState;
                  })
                }
                minHeight={"300px"}
              />
              <div
                style={{
                  display: "flex",
                  justifyContent: "end",
                  alignItems: "center",
                }}
              >
                <Button
                  style={{ marginTop: "20px" }}
                  variant="negative"
                  onClick={() => {
                    setGlobalData((prevState) => {
                      const newData = [...prevState];
                      newData.splice(idx, 1);
                      return newData;
                    });
                    const currentIdx = parseInt(currentTab.split("-")[1]);
                    setCurrentTab(
                      `accordion-${currentIdx >= 1 ? currentIdx - 1 : 0}`
                    );
                  }}
                >
                  Delete Accordion
                </Button>
              </div>
            </Tabs.Panel>
          );
        })}
      </Tabs>
    </>
  );
};

Is there something I'm doing wrong when rendering the editors? Or is this a bug of the editor? Version 1 did not have this problem using the same code structure, but it did have problems selecting the text and changing it.

niclaszllaudi commented 2 years ago

Hi, I encountered the same issue with basically the same use case. After some debugging I tracked down the issue to the ids of the respective ConnectedRichTextEditors. Internally they are passed to the Plate component, which requires the ids to be unique. I was able to fix the issue by passing a unique id to our custom ConnectedRichTextEditor implementation which replaces the internal id calculation.

Unfortunately there doesn't seem to be a fix available without rewriting the internal rich text editor logic.

giovanni-caiazzo commented 2 years ago

Thank you so much @niclaszllaudi for pinpointing the issue in the codebase. I have used patch-package to make it work. This however is NOT a solution to the issue and if the devs want us to make a PR I would happily do it. For now my project is not blocked though, which is great.

z0al commented 2 years ago

I was able to fix the issue by passing a unique id to our custom ConnectedRichTextEditor implementation which replaces the internal id calculation.

Can you share more about your use case @niclaszllaudi & @giovanni-caiazzo ?

For the internal ids calculation we use the entry key + field id + the locale code. So, in theory, it should only clash if you are rendering the same entry field for the same locale twice. Is that the case for you?

giovanni-caiazzo commented 2 years ago

Hi @z0al. In my use case I have a contentful app that is rendering a json field as a collection of rich text fields, each accessible with its own tab. I used patch-package to add a new prop to ConnectedRichEditor in order to add a custom ID appended to the ID you are creating like you described.

niclaszllaudi commented 2 years ago

Hi @z0al, we want to render two ConnectedRichTextEditor instances within the same field. This results in both being assigned the same id, causing the issue.

github-actions[bot] commented 2 years ago

Marking issue as stale since there was no acitivty for 30 days