我正在尝试制作一个文本编辑器应用程序,其中用户可以拥有多个编辑器,例如 notion,我使用 Slate.js、React、TypeScript 制作了文本编辑器。目前我只有一个编辑器,我将其内容存储在 localStorage 中。
我还实现了一个显示编辑器的侧边栏,当用户选择任何编辑器时,当前编辑器状态会发生变化,但 Slate 编辑器中的内容不会改变。虽然标题发生了变化,这意味着
currentEditor
状态正在发生变化,但即使在传递新编辑器的值之后,我也无法更改编辑器的内容。
之前我使用的是自定义钩子。现在我已经转向 Redux Toolkit 来管理编辑器。
附上我的代码供参考:
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: {
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));
}
}
},
},
我想知道,每次用户更改时,我们是否必须初始化一个新的板岩编辑器,或者仅通过传递
initialValue
来更改编辑器的 currentEditor.value
属性,还是必须使用路由来更改 initialValue
的
Slate
。非常感谢您的帮助。
您似乎遇到了这样的问题:当您在不同编辑器之间切换时,Slate.js 编辑器的内容不会更新,即使
currentEditor
状态发生更改并正确更新标题。此行为表明 Slate 编辑器没有根据新的 currentEditor.value
重新初始化或更新其内容。让我们解决您在不同编辑器之间切换时如何更新编辑器内容的问题。
强制 React 组件重新初始化的一种常见方法是更改其
key
属性。 React 使用 key
属性来确定是否应该重新渲染组件或重用以前的实例。通过分配一个随 currentEditor
变化而变化的唯一键,您可以强制 React 使用新内容重新初始化 Slate 编辑器。
在渲染
App.tsx
的 EditorComponent
中,您可以添加一个关键道具,如下所示:
<EditorComponent
key={currentEditor.id}
editor={editor}
renderElement={renderElement}
renderLeaf={renderLeaf}
/>
这将导致
EditorComponent
在您切换编辑器时重新安装,确保编辑器使用正确的 currentEditor.value
进行初始化。
另一种方法是在更高级别管理 Slate 的值状态,并在
currentEditor
发生变化时显式更新它。这涉及将状态提升到 App
组件或通过 Redux 存储对其进行管理,然后将编辑器的值作为 prop 传递给您的 EditorComponent
。
首先,确保您的
EditorComponent
接受编辑器值的新 prop 并将其用作 Slate 的初始值。您可能还需要管理编辑器值的本地状态,以便在更改事件时更新它。这是一个简化的示例:
const EditorComponent = ({ editor, renderElement, renderLeaf, initialValue }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
// Update local state when initialValue changes
setValue(initialValue);
}, [initialValue]);
return (
<Slate
editor={editor}
value={value}
onChange={newValue => setValue(newValue)}
>
{/* Your editor setup */}
</Slate>
);
};
然后,在您的
App.tsx
中,将 currentEditor.value
作为 initialValue
属性传递给 EditorComponent
:
<EditorComponent
editor={editor}
renderElement={renderElement}
renderLeaf={renderLeaf}
initialValue={currentEditor.value}
/>
两种方法都有各自的用例。使用
key
属性是一个更简单的解决方案,它强制编辑器组件完全重新初始化,这可能适合您的情况。从外部管理值状态可以为您提供更多控制,对于更复杂的场景(在编辑器的内容更改时需要执行其他操作)可能是必要的。
请记住,在使用 Redux 等外部状态管理时,请确保状态更新正确触发组件中的重新渲染。调试状态管理问题可能很棘手,因此请确保在编辑器之间切换时验证状态是否按预期更新。