【问题标题】:Is using useImperativeHandle hook justified here with Draft.JS editor and Formik?在 Draft.JS 编辑器和 Formik 中使用 useImperativeHandle 钩子是否合理?
【发布时间】:2021-02-26 10:54:38
【问题描述】:

我正在使用 React 制作一个小型 CMS 系统,并且我有一个表单,用户可以在其中使用 Draft.js 编辑器以及其他一些字段。对于心中的问题,让我们专注于编辑表单。

编辑器的代码如下所示:

import React, { useRef } from 'react';
import { Formik } from "formik";
import TextInputField from "@/components/TextInputField";
import client from "@/utils/http";

const MyForm = ({title, content}) => {
   const editorRef = useRef();
  
  function handleSubmit(values) {
     const editorContent = editorRef.current.parse();
     
     client.submit('/api/edit/project', { editorContent, ...values });
  }

  return (
    <Formik onSubmit={formik.handleSubmit} initialValues={{ title }}>
     {
       (formik) => (
           <form onSubmit={formik.handleSubmit}>
              <TextInputField label="title" name="title" />

              <RichEditor ref={editorRef} content={content} />
           </form>
     )}
  </Formik>);
}

我有编辑器代码:

import React, { useImperativeHandle, useState } from "react";
import {
  Editor,
  EditorState,
  convertFromHTML,
  ContentState,
  convertToRaw,
} from "draft-js";
import draftToHtml from "draftjs-to-html";

function createFromContent(htmlContent) {
  const blocksFromHtml = convertFromHTML(htmlContent);

  const editorState = ContentState.createFromBlockArray(
    blocksFromHtml.contentBlocks,
    blocksFromHtml.entityMap
  );

  return EditorState.createWithContent(editorState);
}

function formatToHTML(editorState) {
  const raw = convertToRaw(editorState.getCurrentContent());
  const markup = draftToHtml(raw);
  return markup;
}

function RichEditor({ content = null }, ref) {
  const [editorState, setEditorState] = useState(() =>
    content ? createFromContent(content) : EditorState.createEmpty()
  );

  useImperativeHandle(
    ref,
    () => ({
      parse: () => {
        return formatToHTML(editorState);
      },
    }),
    [editorState]
  );

  return (
    <div className="App-Rich-Editor w-full block border border-gray-300 rounded-md mt-4 shadow-sm">
      <Editor
        placeholder="Enter your content..."
        editorState={editorState}
        onChange={setEditorState}
      />
    </div>
  );
}

export default React.forwardRef(RichEditor);

有效,但它让我想到了以下问题,因此为什么要问社区,因为使用 useImperativeHandle 似乎是“黑客”。因为即使是 React 文档也不鼓励使用它。

与往常一样,在大多数情况下应避免使用 refs 的命令式代码。

因为我想格式化编辑器的内部状态只格式化一次,所以当我提交表单时,我显示的代码是否合理,即使它“逆流而上”,通过使用命令句柄与父共享子状态。

这让我想到了问题:

  • 在这种情况下是否可以使用 useImperativeHandle 挂钩,以进行“优化”,以便我们仅在需要时才获取状态?
  • 是否有更好的方法可以使用“常规”模式(例如“提升状态”、“渲染道具”或其他方式)来实现此实现?
  • 我是否忽略了这里的问题,我是否应该硬着头皮将整个编辑器状态与formik 同步,将其从组件中抬起,然后在提交时对其进行格式化?

对我来说,第三种选择似乎打破了关注点的分离,因为它会用状态逻辑污染Form 上下文,感觉它不属于那里。

【问题讨论】:

    标签: reactjs react-hooks formik draftjs


    【解决方案1】:

    在我看来,提供的解决方案有点过度设计。因此,让我谈谈我对您提出的问题的看法:

    • 我看不到使用 useImperativeHandle 的优化,因为值同时存储在 ref 和 RichEditor 状态中
    • formatToHTML 函数似乎是纯函数。那么为什么不导出它并在表单提交之前使用它,而不是用forwardRefuseImperativeHandle 使事情复杂化
    • 这是我的建议,我认为这正是您在第 3 条中提到的内容:
    import TextInputField from "@/components/TextInputField";
    import client from "@/utils/http";
    import {
      ContentState,
      convertFromHTML,
      convertToRaw,
      Editor,
      EditorState,
    } from "draft-js";
    import draftToHtml from "draftjs-to-html";
    import { Formik } from "formik";
    import React, { useCallback } from "react";
    
    function createFromContent(htmlContent) {
      const blocksFromHtml = convertFromHTML(htmlContent);
    
      const editorState = ContentState.createFromBlockArray(
        blocksFromHtml.contentBlocks,
        blocksFromHtml.entityMap
      );
    
      return EditorState.createWithContent(editorState);
    }
    
    function formatToHTML(editorState) {
      const raw = convertToRaw(editorState.getCurrentContent());
      const markup = draftToHtml(raw);
      return markup;
    }
    
    const MyForm = ({ title, content }) => {
      const [editorState, setEditorState] = useState(() =>
        content ? createFromContent(content) : EditorState.createEmpty()
      );
    
      const handleSubmit = useCallback(
        (values) => {
          const editorContent = formatToHTML(editorState);
    
          client.submit("/api/edit/project", { editorContent, ...values });
        },
        [editorState]
      );
    
      return (
        <Formik onSubmit={handleSubmit} initialValues={{ title }}>
          {(formik) => (
            <form onSubmit={formik.handleSubmit}>
              <TextInputField label="title" name="title" />
    
              <div className="App-Rich-Editor w-full block border border-gray-300 rounded-md mt-4 shadow-sm">
                <Editor
                  placeholder="Enter your content..."
                  editorState={editorState}
                  onChange={setEditorState}
                />
              </div>
            </form>
          )}
        </Formik>
      );
    };
    
    export default MyForm;
    

    【讨论】:

    • 可以理解,只是一个简单的后续问题,在其余状态存在的上层上下文中从编辑器中“泄漏”内部状态不被认为是不好的封装做法吗?我完全理解这被称为“提升状态”,但这违背了良好代码设计和“封装”的原则之一。 “提升状态”是这个一般规则的例外吗?对于“优化”,是的,你帮助我意识到我并没有真正“优化”任何东西,我什至增加了更多不必要的复杂性。
    • “提升状态”本身并没有破坏封装。当 1 个组件包含一些对 2 个组件有假设的代码时,就会发生封装的破坏。例如,A 具有a,B 代码包含一个假设,即当 B 中的 a 更改为 aa 代码中断时,A 具有 a
    • 在这里,当您传递editorState 时,您还传递了setEditorState,它在MyForm 中处理,而不是在Editor 中。因此,EditoreditorState 中的内容没有任何假设,您可以在MyForm 级别传递一些其他回调,而不是setEditorState,这意味着editorState 的更改方式在MyForm 中定义。更改仅在Editor 中触发。
    • 可以理解,感谢跟进,感谢解答
    猜你喜欢
    • 2016-10-30
    • 1970-01-01
    • 2021-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-24
    • 2021-09-05
    相关资源
    最近更新 更多