Check out bidbear.io Amazon Advertising for Humans. Now publicly available 🚀

Storing TipTap Editor State in Redux with Debouncing

Contents

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

TipTap.js
// 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. 🙌

Amazon Ad Analytics For Humans

Advertising reports automatically saved and displayed beautifully for powerful insights.

bidbear.io
portfolios page sunburst chart