Storing TipTap Editor State in Redux with Debouncing
Intro
I'm using TipTap for a project and I wanted to store the editor state in Redux. I also wanted to debounce the state updates so that I wasn't storing the state on every single change, which I can tell you will crash your application immediately. But why would we want to store the TipTap state in Redux anyways? Well one of the most annoying behaviors that it is possible to encounter online is when you are filling out a form and then you reload the page and the content of the form is gone. Throwing away potentially hours of work. So I wanted to store the TipTap state in Redux so that I could persist the editor state in local storage and then rehydrate the editor state when the user returns to the page.
The Code
// redux
import { useState, useEffect } from "react";
import { connect } from "react-redux";
// tiptap
import {
setContent,
setMetaData,
clearTipTapState,
} from "../redux/tipTapSlice.js";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
// components
import { ReactComponent as Bold } from "./icons/quill/bold.svg";
import { ReactComponent as Italic } from "./icons/quill/italic.svg";
const extensions = [
StarterKit.configure({
bulletList: {
keepMarks: true,
keepAttributes: false,
},
orderedList: {
keepMarks: true,
keepAttributes: false,
},
}),
];
const Tiptap = ({
// data
content,
// actions
setContent,
}) => {
// we are going to use the onUpdate hook to track changes to the content
const [contentRecentlyChanged, setContentRecentlyChanged] = useState(false);
const editor = useEditor({
extensions: extensions,
content: content,
onUpdate({ editor }) {
if (!contentRecentlyChanged) {
setContentRecentlyChanged(true);
}
},
});
useEffect(() => {
const saveInterval = setInterval(() => {
if (editor && contentRecentlyChanged) {
const currentContent = editor.getJSON();
console.log("Auto-saving content", currentContent);
setContent(currentContent);
setContentRecentlyChanged(false);
}
}, 2000); // save every 2 seconds
return () => clearInterval(saveInterval); // Cleanup interval
}, [editor, setContent, contentRecentlyChanged]);
const renderMenu = () => {
if (!editor) {
return null;
} else {
return (
<div className="flex-row flex-wrap">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
className={`${
editor.isActive("bold") ? "is-active" : ""
} wysiwyg-menu`}
>
<Bold />
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
className={`${
editor.isActive("italic") ? "is-active" : ""
} wysiwyg-menu`}
>
<Italic />
</button>
{/* ... the rest of the menu... */}
</div>
);
}
};
const renderButtons = () => {
let handleClick = () => {
console.log("clicked");
};
return (
<button className="green" onClick={() => handleClick()}>
Save Comment
</button>
);
};
if (!editor) {
return null;
}
return (
<div className="tiptap content-card p-1">
{renderMenu()}
<EditorContent editor={editor} />
{renderButtons()}
</div>
);
};
// map state to props
function mapState(state) {
return {
// universal
content: state.tiptap.content,
};
}
// map actions to props
const mapDispatch = {
setContent,
setMetaData,
clearTipTapState,
};
export default connect(mapState, mapDispatch)(Tiptap);
The Explanation
One of the key components here is the TipTap onUpdate hook, which let's us fire off some code every time the state of the editor changes (the user is typing). However if we just used that hook to store the state on every key press you will quickly crash the application. We need to "debounce" the state updates. To do that we can create a useState state called contentRecentlyChanged
which is a boolean that will be set true (if it is not already) every time the editor state changes, using the tipTap onUpdate hook.
Then all we need to do is create a useEffect hook which runs on a timed loop. Every two seconds it will check to see if the user has made any changes to the editor recently. If that boolean is true then we will store the editor state to redux and reset that boolean.
The end result is that as long as the user is actively typing, the state will be stored to redux every two seconds, and if the user stops typing then the state updates will stop. The saves us from pointlessly saving the state hundreds of times if the user has stepped out to get a cup of coffee. 🙌
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.