はじめに
今回、名刺アプリの自己紹介欄で「HTMLタグを含む文字列をそのまま表示したい」という要件に取り組みました。
例えば、データベースに保存されている文字列が以下のような場合:
<h1>僕の名前はJohnです</h1>
<p style="color: red;">赤色のテキストです。</p>
これを普通にReactで表示すると、HTMLタグがエスケープされて文字列としてそのまま表示されてしまいます。でも今回は、HTMLとして解釈して、見出しとして大きく表示したり、赤色で表示したりしたいというのが目的でした。
注意点
セキュリティリスクについて
dangerouslySetInnerHTML
という名前の通り、これは危険な機能です。
なぜ危険なのか?
- ユーザーが入力したHTMLをそのまま表示すると、XSS(クロスサイトスクリプティング)攻撃のリスクがある
- 悪意のあるJavaScriptコードが埋め込まれる可能性
<!-- 悪意のある簡単な例 -->
<script>alert('あなたのクッキーを盗みます');</script>
安全に使うためには:
- 信頼できるソースからのHTMLのみを使用する
- ユーザー入力をそのまま使う場合は、必ずサニタイズ(無害化)処理を行う
- 可能であれば、DOMPurifyなどのライブラリを使う
今回のケースでは、個人学習用の小さいアプリで外部公開しないものなので、サニタイズ処理は省略しています。実際の本番環境や外部公開するアプリでは、必ずサニタイズ処理を入れましょう。
dangerouslySetInnerHTMLの基本的な使い方
通常の方法(HTMLがエスケープされる)
// HTMLタグを含む文字列
const htmlString = "<h1>僕の名前はJohnです</h1>";
// これだとHTMLタグが文字列として表示される
<div>{htmlString}</div>
dangerouslySetInnerHTMLを使う方法
// HTMLタグを含む文字列
const htmlString = "<h1>僕の名前はJohnです</h1>";
// HTMLとして解釈される
<div dangerouslySetInnerHTML={{ __html: htmlString }} />
なぜ__html
というプロパティ名?
アンダースコア2つ(__
)で始まる名前は、「これは危険な機能ですよ!使う前によく考えてね!」という警告の意味が込められています。開発者が意図的にこの名前を書かないといけないようにすることで、無意識に危険な機能を使ってしまうことを防いでいます。
Chakra UIと組み合わせる場合の注意点
今回はChakra UIを使っていたので、少し工夫が必要でした。
問題:Chakra UIのスタイルが優先されてしまう
最初はText
コンポーネントを使っていました:
<Text
fontSize={{ base: "md" }}
fontWeight="medium"
color="fg"
dangerouslySetInnerHTML={{ __html: user.description }}
/>
しかし、これだとHTMLの<h1>
タグを埋め込んでも、Chakra UIのText
コンポーネントのスタイル(fontSize: md
など)が優先されてしまい、見出しとして表示されませんでした。
解決策:Boxコンポーネント + cssプロップ
<Box
bg="bg.muted"
p={{ base: 2, sm: 3 }}
borderRadius="md"
fontSize={{ base: "md" }}
lineHeight="1.6"
wordBreak="break-word"
css={{
"& *": {
all: "revert",
color: "inherit",
},
"& h1, & h2, & h3, & h4, & h5, & h6": {
fontWeight: "bold",
marginTop: "0.5em",
marginBottom: "0.5em",
},
"& h1": { fontSize: "2em" },
"& h2": { fontSize: "1.5em" },
"& h3": { fontSize: "1.17em" },
"& p": {
marginTop: "0.5em",
marginBottom: "0.5em",
},
}}
dangerouslySetInnerHTML={{ __html: user.description }}
/>
各CSSプロパティの意味
all: "revert"
すべてのスタイルをブラウザのデフォルトに戻します。Chakra UIが適用したスタイルをリセットして、HTMLタグ本来の見た目を取り戻すための設定です。
color: "inherit"
親要素の色を継承します。これにより、ライトモード・ダークモードの切り替えに対応できます。
"& h1, & h2, & h3, & h4, & h5, & h6"
&
は「この要素の中にある」という意味です。つまり、& h1
は「このBox要素の中にあるh1要素」を指します。
見出しタグに共通のスタイル(太字、上下のマージン)を適用しています。
fontSize: "2em"
相対的なフォントサイズです。2em
は親要素のフォントサイズの2倍という意味で、これによりh1が大きく表示されます。
実際にテストしてみた結果
以下のようなテスト用HTML文字列を作成して確認しました:
const testDescription = `
<h1>見出し1のテスト</h1>
<h2>見出し2のテスト</h2>
<p>通常のパラグラフです。</p>
<p style="color: red;">赤色のテキストです。</p>
<p style="color: blue; font-weight: bold;">青色で太字のテキストです。</p>
<p style="color: green; font-size: 20px;">緑色で大きいテキストです。</p>
<p><span style="background-color: yellow;">黄色の背景</span>のテキストです。</p>
<h3 style="color: purple;">紫色の見出し3</h3>
<p style="font-style: italic; color: orange;">オレンジ色のイタリック体です。</p>
`;
結果、すべてのスタイルが正しく適用されました!
- 見出しがそれぞれ異なるサイズで表示される
- 色(red, blue, green, purple, orange)が正しく反映される
- 太字、イタリック体も適用される
- 背景色(yellow)も表示される
まとめ
-
dangerouslySetInnerHTML
を使うとReactでHTMLを動的に埋め込める - 二重の波括弧
{{ __html: content }}
の構文を使う - セキュリティリスクがあるので、信頼できるデータのみに使う
- Chakra UIなどのUIライブラリと組み合わせる場合は、CSSで調整が必要
-
all: "revert"
でスタイルをリセットできる
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてください!
▼▼▼