You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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执行时又创建了新的实例,最终出现两个编辑器。

解决方案

优化点

  1. 初始化编辑器时直接将实例赋值给editorRef.current,无需等待onReady回调,确保清理函数能获取到实例并销毁。
  2. 添加判断逻辑,避免重复创建编辑器实例。

修改后的完整代码

"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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 09:27:11