JavaScript
ECMAScript
React
ECMAScript2015

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

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

See the Pen React: Formatting text with markdown by Fumio Nonaka (@FumioNonaka) on CodePen.


create-react-appでReactのひな形アプリケーションをつくる

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

qiita_12_002_003.png


コメントリストをつくる

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

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


マークダウンのライブラリを使う

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

$ npm install remarkable --save

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

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


dangerouslySetInnerHTMLプロパティによりHTMLのコードを差し込む

生のHTMLコードが差し込めてしまうと、「クロスサイトスクリプティング」(XSS)による攻撃を受けるかもしれません(「クロスサイトスクリプティング対策 ホンキのキホン」参照)。そのため、ReactはHTMLのタグは、そのままでは加えられないようにしたのです。

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

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


配列データからテキストを取り出して差し込む

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

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でつくったアプリケーションと少し違いますので、その点だけお気をつけください。

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;