React: マークダウンの複数コメントをHTMLに変換してリストにする例

本稿では、Reactで簡単なリストをつくってみます。コンポーネントはクラスで定め、複数データをリストにするというサンプルです。また、[gumi Inc. Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gumi)の12月2日「[ReactのdangerouslySetInnerHTML使ってみた](https://qiita.com/hiromoon/items/f3ed77abd338139ba97b)」を受けて、マークダウンのテキストをライブラリで変換してページに差し込んでみます。

<p data-height="360" data-theme-id="0" data-slug-hash="qQwoWK" data-default-tab="js,result" data-user="FumioNonaka" data-pen-title="React: Formatting text with markdown" class="codepen">See the Pen <a href="https://codepen.io/FumioNonaka/pen/qQwoWK/">React: Formatting text with markdown</a> by Fumio Nonaka (<a href="https://codepen.io/FumioNonaka">@FumioNonaka</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


# create-react-appでReactのひな形アプリケーションをつくる
まず、Reactアプリケーションのジェネレータ`create-react-app`で、`my-app`という名前のひな形アプリケーションをつくります。`create-react-app`のインストールやひな形のつくりかたについては、「[React: Visual Studio Codeで開発環境を整える](https://qiita.com/fumio_nonaka/items/4b82e4e3757f226df6b0)」をお読みください。アプリケーション名のフォルダがつくられ、依存関係を含めた必要なファイルがつぎのように納められます。

![qiita_12_002_003.png](https://qiita-image-store.s3.amazonaws.com/0/262045/a806aca1-77e9-99aa-df5e-ad01418c0d0f.png)


# コメントリストをつくる
ひな形の`src/App.js`をつぎのように書き替えましょう。3つのコンポーネントをクラスで定めています。いずれのクラスも、備えているのは`render()`メソッドだけです。メソッドの戻り値が、HTMLページに差し込まれるテンプレートになります。テンプレートの要素に属性のかたちで与えた値を取り出すのがプロパティ`props`です。各コンポーネントのコードは少ないので、モジュール分けせずひとつのJavaScript(JS)ファイルとします。

```react
import React, { Component } from 'react';
import './App.css';

class App extends Component {
	render() {
		return (
			<div className="comment-box">
				<h1>世界の金言</h1>
				<CommentList />
			</div>
		);
	}
}
class CommentList extends Component {
	render() {
		return (
			<div>
				<Comment author="ヘンリー・キッシンジャー">チャンスは__貯金__できない。</Comment>
				<Comment author="マーク・トウェイン">禁煙なんてたやすい。私は*何千回*もやった。</Comment>
			</div>
		);
	}
}
class Comment extends Component {
	render() {
		return (
			<div>
				<h2>
				{this.props.author}
				</h2>
				{this.props.children}
			</div>
		);
	}
}
export default App;
```

ブラウザでアプリケーションのページを開くと、テンプレートにしたがって要素の構造がつくられ、コメントのリストとして表示されます。

![qiita_12_003_001.png](https://qiita-image-store.s3.amazonaws.com/0/262045/6f4ddbed-01a4-d2cc-79b5-99240099ded2.png)


# マークダウンのライブラリを使う
コメントのテキストにはアスタリスク(`*`)やアンダースコア(`_`)が入っています。これらをマークダウンで表示しようということです。ライブラリとしては[remarkable](https://github.com/jonschlinkert/remarkable#remarkable)を使うことにします。`README.md`にしたがって、`npm`でインストールしてください。

```terminal
$ npm install remarkable --save
```

`Remarkable()`コンストラクタでつくったインスタンスに`render()`メソッドでマークダウンテキストを渡せば、HTMLのフォーマットに変換されたテキストが返されます。コンポーネント(`Comment`)をつぎのように書き替えましょう。

```react
import Remarkable from 'remarkable';

class Comment extends Component {
	markDown = new Remarkable();
	render() {
		return (
			<div>

				<span>
				{this.markDown.render(this.props.children.toString())}
				</span>
			</div>
		);
	}
}
```

ただし、このままではタグがテキストとして示されてしまいます。

![qiita_12_003_002.png](https://qiita-image-store.s3.amazonaws.com/0/262045/1eaec5ca-287e-7988-5088-1059a6fabbfa.png)


# dangerouslySetInnerHTMLプロパティによりHTMLのコードを差し込む
生のHTMLコードが差し込めてしまうと、「[クロスサイトスクリプティング](http://e-words.jp/w/クロスサイトスクリプティング.html)」(XSS)による攻撃を受けるかもしれません(「[クロスサイトスクリプティング対策 ホンキのキホン](https://codezine.jp/article/detail/9149)」参照)。そのため、ReactはHTMLのタグは、そのままでは加えられないようにしたのです。

テキストをHTMLとして差し込むためには、`dangerouslySetInnerHTML`プロパティを用いなければなりません。与えるのはオブジェクトで、プロパティ`__html`にHTMLコードを値として定めます(「[ReactのdangerouslySetInnerHTML使ってみた](https://qiita.com/hiromoon/items/f3ed77abd338139ba97b)」参照)。コンポーネント(`Comment`)はさらにつぎのように書き替えましょう。こうすれば、要素にはRemarkableの`render()`メソッドから返されたマークダウンテキストが、HTMLとして描かれます。

```react
class Comment extends Component {
	rawMarkup() {
		const markDown = new Remarkable();
		const rawMarkup = markDown.render(this.props.children.toString());
		return { __html: rawMarkup };
	}
	render() {
		return (
			<div>

				<span dangerouslySetInnerHTML={this.rawMarkup()} />
			</div>
		);
	}
}
```

![qiita_12_003_003.png](https://qiita-image-store.s3.amazonaws.com/0/262045/793e1dcf-d97d-01fc-cc55-435a5da21851.png)


# 配列データからテキストを取り出して差し込む
コメントとして表示するデータは、つぎのように配列にしてアプリケーション(`App`)のプロパティ(`data`)にもたせましょう。それを子コンポーネント(`CommentList`)の属性に与えます。そのデータは、`Array.map()`メソッドで取り出され、さらにその子のコンポーネント(`Comment`)のテンプレートがつくられるという流れです。アプリケーションのページの見た目は変わりません。

```react
class App extends Component {
	data = [
		{id: 1, author: "ヘンリー・キッシンジャー", text: "チャンスは__貯金__できない。"},
		{id: 2, author: "マーク・トウェイン", text: "禁煙なんてたやすい。私は*何千回*もやった。"}
	];
	render() {
		return (
			<div className="comment-box">
				<h1>世界の金言</h1>
				<CommentList data={this.data} />
			</div>
		);
	}
}
class CommentList extends Component {
  	render() {
		const commentNodes = this.props.data.map((comment) => {
			return (
				<Comment author={comment.author} key={comment.id}>
				{comment.text}
				</Comment>
			);
		});
		return (
			<div>
			{commentNodes}
			</div>
		);
	}
}
```

ここまで書き終えた`src/App.js`のコードは、以下にまとめたとおりです。冒頭のCodePenのサンプルでも中身はお確かめいただけます。ただし、ライブラリの読み込み方とその参照の仕方が、`create-react-app`でつくったアプリケーションと少し違いますので、その点だけお気をつけください。

```react
import React, { Component } from 'react';
import Remarkable from 'remarkable';
import './App.css';

class App extends Component {
	data = [
		{id: 1, author: "ヘンリー・キッシンジャー", text: "チャンスは__貯金__できない。"},
		{id: 2, author: "マーク・トウェイン", text: "禁煙なんてたやすい。私は*何千回*もやった。"}
	];
	render() {
		return (
			<div className="comment-box">
				<h1>世界の金言</h1>
				<CommentList data={this.data} />
			</div>
		);
	}
}
class CommentList extends Component {
  	render() {
		const commentNodes = this.props.data.map((comment) => {
			return (
				<Comment author={comment.author} key={comment.id}>
				{comment.text}
				</Comment>
			);
		});
		return (
			<div>
			{commentNodes}
			</div>
		);
	}
}
class Comment extends Component {
	rawMarkup() {
		const markDown = new Remarkable();
		const rawMarkup = markDown.render(this.props.children.toString());
		return { __html: rawMarkup };
	}
	render() {
		return (
			<div>
				<h2>
				{this.props.author}
				</h2>
				<span dangerouslySetInnerHTML={this.rawMarkup()} />
			</div>
		);
	}
}
export default App;
```