Reputation: 33
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
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