🩹

bytemdにオブジェクトのプロパティを渡すためのひと工夫

2021/07/04に公開

いわゆる、ワークアラウンド用のまとめです。

Bytemdとは

bytemdはWYSIWYGなMarkdown編集を提供するJSコンポーネントです。
シンプルな組み込みで、それなりにきれいなエディター環境を提供してくれます。

本体としてはSevelte用であることが前提なのですが、公式側でReact,Vue向けのラッパーコンポーネントを提供してくれており、
Reactアプリケーションにも簡単に組み込めます。

デモも公開されているので、一度覗いてみてください。

プレビュー用

困りごと

ここからは、@bytemd/reactを利用したReactアプリケーションへの組み込みを前提とした話をします。[1]

ごくごくシンプルに使う

単純にMarkdownコンテンツをstateとして扱うだけなら、これだけで十分です。

import { useState } from "react";
import { Editor } from "@bytemd/react";
import "bytemd/dist/index.min.css";

export default function App() {
  const [body, setBody] = useState("");
  return (
    <>
      <Editor value={body} onChange={setBody} />
      <pre>{body}</pre>
    </>
  );
}

※プレビュー用CodSandboxの、Simple usage

エディター部分の変種がリアルタイムで右ペインのプレビュー領域に表示されています。
エディター外の下部もリアルタイムでbodyの中身が表示されるようになっています。

オブジェクトの一要素を編集するなら...?

とはいえ、Atomicなコンポーネントならともかく、一般的には複数の要素を統合して扱うことも多いでしょう。
そのようなケースでは、以下のような書き方をすることになるのではないでしょうか。

import { useState } from "react";
import { Editor } from "@bytemd/react";
import "bytemd/dist/index.min.css";

export default function App() {
  // 実際はどこかから取ってきたエンティティなどが入る
  const [content, setContent] = useState({ title: "", body: "" });
  return (
    <>
      title:
      <input
        value={content.title}
        onChange={(e) => setContent({ ...content, title: e.target.value })}
      />
      <Editor
        value={content.body}
        onChange={(body) => setContent({ ...content, body })}
      />
      <pre>{JSON.stringify(content)}</pre>
    </>
  );
}

※プレビュー用CodSandboxの、Have problems

さて、CodeSandboxなどでこのコンポーネントを表示させて、タイトル用Inputタグとエディター領域を編集してみてください。

厄介なことに、エディター部分でbodyを編集した瞬間にtitleの中身がリセットされてしまいます。
これは困った。

コードを頑張って追う感じでは、@bytemd/reactは内部でrefを作成しているようです。
どうもrefに渡した時点のオブジェクト要素として管理されてしまい、
onChangeのタイミングでもref時点のcontentしか渡ってこない...みたいです。[2][3]

どう対処する(した)か

こうなりました。

import { useEffect, useState } from "react";
import { Editor } from "@bytemd/react";
import "bytemd/dist/index.min.css";

export default function App() {
  const [content, setContent] = useState({ title: "", body: "" });
  const [body, setBody] = useState(content.body);

  useEffect(() => {
    setContent({ ...content, body });
  }, [body]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <>
      title:
      <input
        value={content.title}
        onChange={(e) => setContent({ ...content, title: e.target.value })}
      />
      <Editor value={content.body} onChange={(body) => setBody(body)} />
      <pre>{JSON.stringify(content)}</pre>
    </>
  );
}

※プレビュー用CodSandboxの、Fixed problems

bodyを別のstateとしつつ、useEffectbodyの変更を検知することで、bodyの変化に対する処理を2段階となるようにしています。
こうすることで、Editorコンポーネント内のrefcontentを間接的にでも参照経路が切れるのか、想定する感じで動くようになります。

よかったよかった。

とはいえ、コードのクリーンさ的にも処理の煩雑さ的にも面倒なワークアラウンドではあるので、どこかのタイミングで修正されるといいんですけども。

脚注
  1. Vueではどうなるかは知らないです。 ↩︎

  2. 深堀してません。現象としてそういうことかな?というぐらいの解釈です。 ↩︎

  3. なお、例えばtitleへ空ではない文字列を代入して開始すると、中身の文字列はonChangeのたびに戻されてしまいます。 ↩︎

GitHubで編集を提案

Discussion