Vatsal Dave
Vatsal Dave

Reputation: 33

Unable to change content of Slate.js component when providing new initialValue, editor content remains same as it was

Im trying to make a text editor app in which user can have multiple editors like notion, i have made the text editor using Slate.js, React, TypeScript. Currently I only have one editor and im storing it's content in localStorage.

I have also implemented a sidebar which displays editors, when user selects any editor, the currentEditor state changes but the content in Slate editor does not change. Although the title gets changed which means currentEditor state is changing but im unable to change content of editor even after passing new editor's value.

Earlier i was using a custom hook. Now i have shifted to Redux Toolkit to manage editors.

Attaching my code for reference: App.tsx:

function App() {
    const currentEditor = useAppSelector((state) => state.editors.currentEditor);

    // to make editor to be stable across renders, we use useState without a setter
    // for more reference
    const [editor] = useState(withCustomFeatures(withReact(createEditor())));

    // defining a rendering function based on the element passed to 'props',
    // useCallback here to memoize the function for subsequent renders.
    // this will render our custom elements according to props
    const renderElement = useCallback((props: RenderElementProps) => {
        return <Element {...props} />;
    }, []);

    // a memoized leaf rendering function
    // this will render custom leaf elements according to props
    const renderLeaf = useCallback((props: RenderLeafProps) => {
        return <Leaf {...props} />;
    }, []);

    if (!currentEditor) {
        return <div>loading</div>;
    }

    return (
        <div className="h-full flex">
            <div className="flex h-screen w-60 flex-col inset-y-0">
                <div className="h-full text-primary w-full bg-white">
                    <NavigationSidebar />
                </div>
            </div>
            <main className="w-full">
                <div className="bg-sky-200 flex flex-col h-screen w-full">
                    <div className="text-center mt-4">
                        <h1 className="text-xl">{currentEditor?.title}</h1>
                    </div>
                    <div className="bg-white mx-auto rounded-md my-10 w-4/5">
                        {currentEditor && (
                            <EditorComponent
                                editor={editor}
                                renderElement={renderElement}
                                renderLeaf={renderLeaf}
                            />
                        )}
                    </div>
                </div>
            </main>
        </div>
    );
}

export default App;

Editor.tsx:

const EditorComponent: React.FC<EditorProps> = ({
    editor,
    renderElement,
    renderLeaf,
}) => {
    const { setSearch, decorate } = useDecorate();
    const dispatch = useAppDispatch();

    const currentEditor = useAppSelector((state) => state.editors.currentEditor);

    if (!currentEditor) {
        return <div>Loading</div>;
    }

    return (
        // render the slate context, must be rendered above any editable components,
        //  it can provide editor state to other components like toolbars, menus
        <Slate
            editor={editor}
            initialValue={currentEditor.value}
            // store value to localStorage on change
            onChange={(value) =>
                dispatch(
                    storeContent({
                        id: currentEditor.id,
                        title: currentEditor.title,
                        value,
                        editor,
                    })
                )
            }
        >
            {/* Toolbar */}
            <Toolbar/>
            <HoveringToolbar />
            {/* editable component */}
            <div className="p-3 focus-within:ring-2 focus-within:ring-neutral-200 focus-within:ring-inset border">
                <Editable
                    spellCheck
                    autoFocus
                    className="outline-none max-h-[730px] overflow-y-auto"
                    renderElement={renderElement}
                    renderLeaf={renderLeaf}
                    decorate={decorate}
                     
                     
                />
            </div>
        </Slate>
    );
};

export default EditorComponent;

NavigationSidebar.tsx:

const NavigationSidebar = () => {
    const editors = useAppSelector((state) => state.editors.editors);
    const currentEditor = useAppSelector((state) => state.editors.currentEditor);
    const dispatch = useAppDispatch();
    return (
        <div className="flex flex-col gap-y-4 items-center">
            <div className="text-center py-4 px-2 border-b-2 w-full">
                <h3 className="font-semibold text-xl text-indigo-500">Editors list</h3>
            </div>
            <div className="w-full">
                <ol>
                    {editors.map((editor) => (
                        <li key={editor.id}>
                            <button
                                className={`w-full py-2 border-b hover:bg-indigo-100 text-center ${
                                    currentEditor!.id === editor.id ? "bg-indigo-200" : ""
                                }`}
                                onClick={() => dispatch(setCurrentEditor(editor.id))}
                            >
                                {editor.title}
                            </button>
                        </li>
                    ))}
                </ol>
                <button
                    className="border-b py-2 w-full hover:bg-indigo-100"
                    onClick={() => dispatch(addNewEditor())}
                >
                    New blank editor
                </button>
            </div>
        </div>
    );
};

editorSlice reducers:

reducers: {
        addNewEditor: (state) => {
            const newEditor: EditorInstance = {
                id: `editor${state.editors.length + 1}`,
                title: `Editor ${state.editors.length + 1}`,
                value: [
                    {
                        type: "paragraph",
                        children: [
                            { text: "This is new editable " },
                            { text: "rich", bold: true },
                            { text: " text, " },
                            { text: "much", italic: true },
                            { text: " better than a " },
                            { text: "<textarea>", code: true },
                            { text: "!" },
                        ],
                    },
                ],
            };
            state.editors.push(newEditor);
            localStorage.setItem("editorsRedux", JSON.stringify(state.editors));
            state.currentEditor = newEditor;
        },
        setCurrentEditor: (state, action: PayloadAction<string>) => {
            const selectedEditor = state.editors.find(
                (editor) => editor.id === action.payload
            );

            if (selectedEditor) {
                state.currentEditor = selectedEditor;
            }
        },
        loadEditorsFromLocalStorage: (state) => {
            const storedEditors = localStorage.getItem("editorsRedux");
            if (storedEditors) {
                state.editors = JSON.parse(storedEditors);
                state.currentEditor = state.editors[0];
            } else {
                const initialEditor: EditorInstance = {
                    id: "editor1",
                    title: "Untitled",
                    value: [
                        {
                            type: "paragraph",
                            children: [
                                { text: "This is editable " },
                                { text: "rich", bold: true },
                                { text: " text, " },
                                { text: "much", italic: true },
                                { text: " better than a " },
                                { text: "<textarea>", code: true },
                                { text: "!" },
                            ],
                        },
                    ],
                };

                state.editors = [initialEditor];
                state.currentEditor = initialEditor;
                localStorage.setItem("editorsRedux", JSON.stringify(state.editors));
            }
        },

        storeContent: (
            state,
            action: PayloadAction<{
                id: string;
                title: string;
                value: Descendant[];
                editor: Editor;
            }>
        ) => {
            const title = action.payload.title;
            const value = action.payload.value;
            const isAstChange = action.payload.editor.operations.some(
                (op) => "set_selection" !== op.type
            );
            if (isAstChange) {
                if (state.currentEditor) {
                    const updatedEditors = state.editors.map((editor) => {
                        if (editor.id === action.payload.id) {
                            return { ...editor, title, value };
                        }
                        return editor;
                    });
                    state.editors = updatedEditors;
                    localStorage.setItem("editorsRedux", JSON.stringify(state.editors));
                }
            }
        },
    },

I want to know that do we have to initialzie a new slate editor every time when user changes or only change the editor's initialValue prop by passing currentEditor.value or do we have to use routing to change the initialValue of Slate. Help is very much appreciated.

Upvotes: 1

Views: 401

Answers (1)

Michael Coxon
Michael Coxon

Reputation: 3535

Warning - this is not much of an answer. But.

I have not figured out what actually makes rendering happen (which I think is the root cause of the above issues), other than component load through the initialValue. I know that something makes it happen, other than that, because I get to see some of the changes. It happens while i'm typing text, for example.

But re-rendering doesn't seem to happen when: I do a browser level reload, because the whole Editable field gets dropped even though I have valid Slate Json content, and I run Transforms.insertNodes(editor, image), again, despite having valid Slate JSON, no render happens so visible change.

I've scoured the documentation and the Q&As and haven't found anything so far. I'd love someone smarter than me point out what is presumably obvious to everyone but me.

Upvotes: 0

Related Questions