Skip to content

Commit

Permalink
Fix update editor.js when rerender (#5321)
Browse files Browse the repository at this point in the history
* Fix update editor.js when rerender

* Add changeset
  • Loading branch information
poulch committed Jan 8, 2025
1 parent ffaa00f commit 081c5c0
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/soft-spoons-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Now category and subcategories show proper description
13 changes: 12 additions & 1 deletion src/components/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import clsx from "clsx";
import React from "react";

import { tools } from "./consts";
import { useHasRendered } from "./hooks";
import { useHasRendered, useUpdateOnRerender } from "./hooks";
import { ReactEditorJS } from "./ReactEditorJS";
import useStyles from "./styles";

Expand Down Expand Up @@ -41,6 +41,7 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
}) => {
const classes = useStyles({});
const id = useId(defaultId);
const ref = React.useRef<EditorCore | null>(null);
const [isFocused, setIsFocused] = React.useState(false);
const [hasValue, setHasValue] = React.useState(false);
const isTyped = Boolean(hasValue || isFocused);
Expand All @@ -54,12 +55,22 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
}

if (editorRef) {
ref.current = editor;

return (editorRef.current = editor);
}
}, []);
// We need to render FormControl first to get id from @reach/auto-id
const hasRendered = useHasRendered();

// EditorJS does not rerender when default value changes,
// so we need to manually update it
useUpdateOnRerender({
render: ref.current?.render.bind(ref.current),
defaultValue: props.defaultValue,
hasRendered,
});

return (
<FormControl
data-test-id={"rich-text-editor-" + name}
Expand Down
93 changes: 93 additions & 0 deletions src/components/RichTextEditor/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { renderHook } from "@testing-library/react-hooks";

import { useUpdateOnRerender } from "./hooks";

describe("useUpdateOnRerender", () => {
it("should call render when defaultValue changes after initial render", () => {
// Arrange
const mockRender = jest.fn();

const { rerender } = renderHook(
({ render, defaultValue, hasRendered }) =>
useUpdateOnRerender({ render, defaultValue, hasRendered }),
{
initialProps: {
render: mockRender,
defaultValue: { blocks: [{ type: "paragraph", data: { text: "Initial" } }] },
hasRendered: true,
},
},
);

// Act
rerender({
render: mockRender,
defaultValue: { blocks: [{ type: "paragraph", data: { text: "Updated" } }] },
hasRendered: true,
});

// Assert
expect(mockRender).toHaveBeenCalledWith({
blocks: [{ type: "paragraph", data: { text: "Updated" } }],
});
});

it("should call render once when defaultValue change", () => {
// Arrange
const mockRender = jest.fn();

const { rerender } = renderHook(
({ render, defaultValue, hasRendered }) =>
useUpdateOnRerender({ render, defaultValue, hasRendered }),
{
initialProps: {
render: mockRender,
defaultValue: { blocks: [{ type: "paragraph", data: { text: "Initial" } }] },
hasRendered: false,
},
},
);

// Act
rerender({
render: mockRender,
defaultValue: { blocks: [{ type: "paragraph", data: { text: "Initial" } }] },
hasRendered: true,
});
rerender({
render: mockRender,
defaultValue: { blocks: [{ type: "paragraph", data: { text: "Initial" } }] },
hasRendered: true,
});

// Assert
expect(mockRender).toHaveBeenCalledTimes(1);
});

it("should not call render if hasRendered is false", () => {
// Arrange
const mockRender = jest.fn();

const { rerender } = renderHook(
({ render, defaultValue, hasRendered }) =>
useUpdateOnRerender({ render, defaultValue, hasRendered }),
{
initialProps: {
render: mockRender,
defaultValue: { blocks: [{ type: "paragraph", data: { text: "Initial" } }] },
hasRendered: false,
},
},
);

// Act
rerender({
render: mockRender,
defaultValue: { blocks: [{ type: "paragraph", data: { text: "Updated" } }] },
hasRendered: false,
});

// Assert
expect(mockRender).not.toHaveBeenCalled();
});
});
33 changes: 32 additions & 1 deletion src/components/RichTextEditor/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useLayoutEffect, useState } from "react";
import { EditorConfig } from "@editorjs/editorjs";
import { EditorCore } from "@react-editor-js/core";
import { useEffect, useLayoutEffect, useRef, useState } from "react";

export const useHasRendered = () => {
const [hasRendered, setHasRendereed] = useState(false);
Expand All @@ -9,3 +11,32 @@ export const useHasRendered = () => {

return hasRendered;
};

export const useUpdateOnRerender = ({
render,
defaultValue,
hasRendered,
}: {
render: EditorCore["render"] | undefined;
defaultValue: EditorConfig["data"];
hasRendered: boolean;
}) => {
const prevDefaultValue = useRef<EditorConfig["data"] | undefined>(undefined);

useEffect(() => {
if (!hasRendered) {
return;
}

// Prevent call render when defaultValue doesn't change
if (JSON.stringify(defaultValue) === JSON.stringify(prevDefaultValue.current)) {
return;
}

prevDefaultValue.current = defaultValue;

render?.({
blocks: defaultValue?.blocks ?? [],
});
}, [defaultValue, hasRendered, render]);
};

0 comments on commit 081c5c0

Please sign in to comment.