やろうとしたこと
- Next.jsで「記事データ(.html)からSSGで静的ブログを作成する」というのをやろうとした。
- やり方としては、
rehypeやrehype-reactを使って、HTML→ReactElementに変換して、変換したReactElementを任意のコンポーネントに埋め込むことにした - このとき、記事データに埋め込んだTweetを、Twitter公式の
https://platform.twitter.com/widgets.jsをuseEffect内で動かして、blockquoteをiframeに展開する・・みたいにしようした - だけど以下のようなエラーが出て上手く行かなかった

▲Unhandled Runtime Error NotFoundError: Failed to execute ‘removeChild’ on ‘Node’: The node to be removed is not a child of this node.
具体的に言うと、以下のような関数を作って
import rehypeParse from "rehype-parse";
import { unified } from "unified";
import rehypeReact from "rehype-react";
import CustomLink from "src/components/utils/CustomLink";
const processor = unified()
.use(rehypeParse, { fragment: true })
.use(rehypeReact, {
createElement: React.createElement,
});
記事データを埋め込みたい部分に
const containerElem = useRef(null);
useEffect(() => {
(window as any).twttr?.widgets?.load(containerElem.current);
}, []);
<div ref={containerElem}>
{processor.processSync(HTMLデータ).result}
</div>
みたいな感じにしていました。
あとTwitterの埋め込みのためのコードは以下のように書いておいて、_app.tsxなどにおいておく感じです。
import Script from "next/script";
export const TwitterEmbed: React.VFC = () => {
return (
<Script id="tweet-show" strategy="afterInteractive" >
{`window.twttr = (function (d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0],
t = window.twttr || {};
if (d.getElementById(id)) return t;
js = d.createElement(s);
js.id = id;
js.src = "https://platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
t._e = [];
t.ready = function (f) {
t._e.push(f);
};
return t;
})(document, "script", "twitter-wjs");
`}
</Script>
);
};
参考:Set up Twitter for Websites | Docs | Twitter Developer Platform
原因
結論から言うと、以下のページに書いてあることが原因でした。
余分な <div> で <select> をラップしていることに注目してください。これが必要なのは、Chosen は渡された <select> の直後に別の DOM 要素を追加するからです。しかし、React からみれば、この <div> は常に 1 つの子要素しか持っていません。これにより React による更新が Chosen によって追加された DOM ノードと確実に競合しないようにできるのです。重要なことは、React フローの外側で DOM を変更する場合は、React がその DOM ノードに触る理由を確実になくす必要がある、ということです。
わかりやすく
※ここから先は「たぶんこういう理屈なんだろう」という私の主観で書いてますので、間違っている場合はコメントいただけると嬉しいです
例えば、以下のようなコンポーネントがあったとします。
const Hoge = () => {
return (
<div>
<h1>タイトルだよ</h1>
<p>本文だよ</p>
</div>
);
};
これを、例えばjQueryを使ってh1を削除したとします。
次に、このコンポーネントがアンマウントされるとします。
するとReactはdivタグを丸ごと削除することでアンマウントします。
このとき、Reactは「h1がすでに削除されている」ことを知りませんが、divごと削除するので特に問題は起きません。
しかし、以下のようなコンポーネントの場合は違います。
const Hoge = () => {
return (
<>
<h1>タイトルだよ</h1>
<p>本文だよ</p>
</>
);
};
これを、例えばjQueryを使ってh1を削除したとします。
次に、このコンポーネントがアンマウントされるとします。
するとReactはh1とpをそれぞれ個別に削除しようとします。囲っているタグがないので1つ1つ削除するしかないからです。
しかし、h1はReactの預かり知らぬところでjQueryによって削除されています。なので削除できません。しかしReactはそれに気づけません。
その結果、すでに存在しないh1を削除するためにremoveChildメソッドを実行してしまい、上のようなエラーが発生する・・・
みたいな理屈だと思われます。
私の例
私の例でいうと
<div ref={containerElem}>
{processor.processSync(HTMLデータ).result}
</div>
と書いていますが、
{processor.processSync(HTMLデータ).result}
の部分はReactElementで、なおかつ、HTMLデータの部分が
<h1>タイトルだよ</h1> <blockquote>ツイートの内容だよ</blockquote>
という形だったとします。
このときTwitterのwidget.jsを動かすと、blockquoteがiframeに書き換わります。
次に、このコンポーネントがアンマウントされるとします。
するとReactはh1とblockquoteをそれぞれ個別に削除しようとします。囲っているタグがないので1つ1つ削除するしかないからです。
しかし、blockquoteはReactの預かり知らぬところでwidget.jsによって削除されています。なので削除できません。しかしReactはそれに気づけません。
その結果、すでに存在しないblockquoteを削除するためにremoveChildメソッドを実行してしまい、上のようなエラーが発生する・・・
みたいな感じです。
対処法
私の場合、
- 記事データ(.html)全体を丸ごとdivタグで囲む
- Tweetのblockquoteタグそれぞれをdivタグで囲む
のいずれかの対処をすると、エラーがでなくなりました。
まとめ
・jQueryなどのライブラリを使って、Reactの管理外で操作する可能性があるReactElementは、divタグとかで囲っておく
おわり
参考にしたページ:
コメント