Next.js 14+TypeScript中EditorJS重复渲染问题的解决方法
问题
在Next.js 14 + TypeScript项目中使用EditorJS时,编辑器出现重复渲染的情况,但仅在id为textEditor的div中进行了初始化,未在其他地方配置该组件。请问出现重复渲染的原因是什么?该如何修改代码让编辑器只渲染一次?
原因分析
- Next.js严格模式的双重执行机制:开发环境下,Next.js的Strict Mode会触发组件的两次渲染(包括
useEffect钩子的两次执行),用于检测潜在的副作用问题。 - 编辑器实例的初始化与清理时机不匹配:原代码中
editorRef.current是在onReady回调中赋值的,而Strict Mode下第一次执行useEffect后会立刻调用清理函数,此时onReady可能还未执行,导致editorRef.current为null,无法销毁第一次创建的编辑器实例;第二次useEffect执行时又创建了新的实例,最终出现两个编辑器。
解决方案
优化点
- 初始化编辑器时直接将实例赋值给
editorRef.current,无需等待onReady回调,确保清理函数能获取到实例并销毁。 - 添加判断逻辑,避免重复创建编辑器实例。
修改后的完整代码
"use client"; import React, { useEffect, useRef, useState } from "react"; import UploadProductImage from "./upload-image"; import { TImage } from "@/types"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { addProductFormSchema } from "@/schemas"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import EditorJS, { OutputData } from "@editorjs/editorjs"; import { tools } from "@/components/editor/editor-tool"; import { Button } from "@/components/ui/button"; const AddProductForm = () => { const [images, setImages] = useState<File[]>([]); const [uploadImages, setUploadImages] = useState<TImage[]>([]); const [editorContent, setEditorContent] = useState<any[]>([]); const form = useForm<z.infer<typeof addProductFormSchema>>({ resolver: zodResolver(addProductFormSchema), defaultValues: { name: "", price: "", stock: "", discountPrice: "", }, }); const editorRef = useRef<EditorJS | null>(null); const onSubmit = async (values: z.infer<typeof addProductFormSchema>) => { console.log(editorContent); }; useEffect(() => { // 避免重复初始化编辑器 if (editorRef.current) return; const initializeEditor = async () => { const editor = new EditorJS({ holder: "textEditor", placeholder: "Start typing here...", tools: tools, onChange: async () => { const content = await editor.saver.save(); setEditorContent(content.blocks); }, }); // 直接将实例赋值给ref,无需等待onReady回调 editorRef.current = editor; }; initializeEditor(); return () => { if (editorRef.current && typeof editorRef.current.destroy === "function") { editorRef.current.destroy(); editorRef.current = null; } }; }, []); return ( <div className="mt-10"> <Button onClick={form.handleSubmit(onSubmit)}>Add Product</Button> <div className="flex lg:flex-row flex-col gap-4"> <div className="bg-white shadow rounded p-4 lg:w-[450px] w-full"> <UploadProductImage images={images} setImages={setImages} setUploadImages={setUploadImages} /> </div> <div className="flex-1 shadow p-4 rounded bg-white"> <h3 className="text-lg font-semibold text-gray-800"> General Information </h3> <Form {...form}> <form className="space-y-4"> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Product Name</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <div className="grid sm:grid-cols-2 grid-cols-1 gap-4"> <FormField control={form.control} name="price" render={({ field }) => ( <FormItem> <FormLabel>Product Price</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="discountPrice" render={({ field }) => ( <FormItem> <FormLabel> Discount Price{" "} <span className="opacity-80 ml-1 font-normal"> (optional) </span> </FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> </div> <div className="grid sm:grid-cols-2 grid-cols-1 gap-4"> <FormField control={form.control} name="stock" render={({ field }) => ( <FormItem> <FormLabel>Stock</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> </div> </form> </Form> </div> </div> <div className="bg-white shadow p-4 rounded my-10"> <h3 className="text-lg font-semibold text-gray-800"> Product Description </h3> <div id="textEditor"></div> </div> </div> ); }; export default AddProductForm;
额外说明
- 生产环境下Strict Mode不会触发双重渲染,该问题仅会在开发环境出现,但优化后的代码可同时兼容开发和生产环境。
- 如果需要保留
onReady回调,可将实例赋值逻辑放在onReady中,但直接赋值的方式能更可靠地确保清理函数执行时实例已存在。
内容的提问来源于stack exchange,提问作者Pavitar Sharma
相关产品推荐
相关产品推荐

