In the world of modern web development, creating rich, interactive user interfaces often involves juggling multiple libraries and frameworks.
In the world of modern web development, creating rich, interactive user interfaces often involves juggling multiple libraries and frameworks. Today, we're going to dive into a common challenge that arises when using the Tiptap rich text editor within a Next.js application, particularly when fetching data from Supabase.
Imagine you're building a product management system. You have a product detail page where administrators can edit various fields, including a rich text description powered by Tiptap. You're using Next.js for your frontend and Supabase as your backend. Everything seems to work fine, except for one pesky issue: when you navigate to the product detail page, all fields populate correctly except for the description. The Tiptap editor appears empty, even though you can see in your React Query devtools that the correct data has been fetched.
This scenario highlights a common pitfall when working with rich text editors and asynchronous data loading. Let's break down the problem and walk through a solution.
The core of the issue lies in the timing of events:
Our challenge is to ensure that the Tiptap editor updates its content once the data is available.
To solve this, we need to make a few key modifications to our Tiptap component:
Here's what our updated Tiptap component looks like:
"use client";
import React, { useEffect, useState, forwardRef, useImperativeHandle } from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Heading from "@tiptap/extension-heading";
import ToolBar from "./toolbar";
interface TiptapProps {
description: string;
onChange: (richText: string) => void;
}
const Tiptap = forwardRef<{ setContent: (content: string) => void }, TiptapProps>(
({ description, onChange }, ref) => {
const [isMounted, setIsMounted] = useState(false);
const editor = useEditor({
extensions: [
StarterKit.configure({}),
Heading.configure({
HTMLAttributes: {
class: "text-xl font-bold",
levels: [2],
},
}),
],
content: description,
editorProps: {
attributes: {
class:
"rounded-md border min-h-[150px] border-input bg-background focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 p-2",
},
},
onUpdate({ editor }) {
onChange(editor.getHTML());
},
});
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (editor && description !== editor.getHTML()) {
editor.commands.setContent(description);
}
}, [description, editor]);
useImperativeHandle(ref, () => ({
setContent: (content: string) => {
editor?.commands.setContent(content);
},
}));
if (!isMounted) {
return null;
}
return (
<div className="flex flex-col justify-stretch min-h-[250px]">
<ToolBar editor={editor} />
<EditorContent editor={editor} />
</div>
);
}
);
Tiptap.displayName = 'Tiptap';
export default Tiptap;
In your product detail page component, you'll need to utilize these new capabilities:
const editorRef = useRef<{ setContent: (content: string) => void } | null>(null);
useEffect(() => {
if (product && !isModified) {
setFormData({
name: product.name,
description: product.description || "",
price: product.price,
cost: product.cost,
});
if (editorRef.current && product.description) {
editorRef.current.setContent(product.description);
}
}
}, [product, isModified]);
// In your JSX
<Tiptap
description={formData.description || ''}
onChange={handleEditorChange}
ref={editorRef}
/>
The toolbar component remains largely unchanged, but it's worth noting how it integrates with the Tiptap editor:
"use client";
import React from "react";
import { type Editor } from "@tiptap/react";
import { Toggle } from "@/components/ui/toggle";
import {Bold, Heading2, Italic, List, ListOrdered, StrikethroughIcon} from "lucide-react";
type Props = {
editor: Editor | null;
};
function ToolBar({ editor }: Props) {
if (!editor) {
return null;
}
return (
<div className="flex gap-3 border border-input rounded-lg p-1 my-2" >
{/* Toolbar buttons */}
</div>
);
}
export default ToolBar;
The toolbar receives the editor instance as a prop, allowing it to interact directly with the Tiptap editor.
By implementing these changes, we've solved the asynchronous content loading issue with Tiptap in our Next.js and Supabase application. The key takeaways are:
This solution ensures a smooth user experience, with all fields, including rich text areas, populating correctly even when data is loaded asynchronously. Happy coding!