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