【Next.js】widgets.jsを使ってTweet埋め込みをした際にNotFoundError

やろうとしたこと

  • Next.jsで「記事データ(.html)からSSGで静的ブログを作成する」というのをやろうとした。
  • やり方としては、rehyperehype-reactを使って、HTML→ReactElementに変換して、変換したReactElementを任意のコンポーネントに埋め込むことにした
  • このとき、記事データに埋め込んだTweetを、Twitter公式のhttps://platform.twitter.com/widgets.jsuseEffect内で動かして、blockquoteをiframeに展開する・・みたいにしようした
  • だけど以下のようなエラーが出て上手く行かなかった
Unhandled Runtime Error NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

▲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 ノードに触る理由を確実になくす必要がある、ということです。

引用:他のライブラリとのインテグレーション – React

わかりやすく

※ここから先は「たぶんこういう理屈なんだろう」という私の主観で書いてますので、間違っている場合はコメントいただけると嬉しいです

例えば、以下のようなコンポーネントがあったとします。

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はh1pをそれぞれ個別に削除しようとします。囲っているタグがないので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を動かすと、blockquoteiframeに書き換わります。

次に、このコンポーネントがアンマウントされるとします。

するとReactはh1blockquoteをそれぞれ個別に削除しようとします。囲っているタグがないので1つ1つ削除するしかないからです。

しかし、blockquoteはReactの預かり知らぬところでwidget.jsによって削除されています。なので削除できません。しかしReactはそれに気づけません。

その結果、すでに存在しないblockquoteを削除するためにremoveChildメソッドを実行してしまい、上のようなエラーが発生する・・・

みたいな感じです。

対処法

私の場合、

  • 記事データ(.html)全体を丸ごとdivタグで囲む
  • Tweetのblockquoteタグそれぞれをdivタグで囲む

のいずれかの対処をすると、エラーがでなくなりました。

まとめ

・jQueryなどのライブラリを使って、Reactの管理外で操作する可能性があるReactElementは、divタグとかで囲っておく

 

おわり

 

参考にしたページ:

JSでツイートを埋め込むときのベストプラクティス

他のライブラリとのインテグレーション – React

React
スポンサーリンク
この記事を書いた人
penpen

1991生まれ。WEBエンジニア。

技術スタック:TypeScript/Next.js/Express/Docker/AWS

フォローする
フォローする

コメント

タイトルとURLをコピーしました