はじめに
最近、React.jsをいじってみているのだが、React.jsをそのまま使おうとすると低レベルすぎて(言語に例えるならアセンブリ言語とか?)使いにくく感じる。
そこで、自分なりの書き方がまとまってきたものを少しずつ書きためていこうと思う。
ちなみに、React.js + TypeScript + Electronな環境で試しています。
さらにいうと、「7行で作るElectronアプリ」で作成したスケルトン(?)を使っています。
Factoryパターン(?)
デザインパターン本を読んだのははるか昔なので、パターンの名前は適当です。
React.Componentを継承して独自のコンポーネントを作成したとき、同じコンポーネントをすこしだけ変えて使いたいときがある。
たとえば、Markdownファイルを読み込んで表示するコンポーネントがあるとしたとき、見た目や動作は同じで、別のファイルを読み込むコンポーネントが必要になったとする。
このとき、似たようなコンポーネントを1から書くのは無駄すぎる。
そんなとき、どうしたらよいのか考えてみた。
基本となるコンポーネント
まずは、元になるコンポーネントを書いておく。
/// <reference path="../../../typings/tsd.d.ts" />
// パスは自分の環境のものなので各自読み替えてください。
import React = require('../../node_modules/react/');
import marked = require('../../node_modules/marked/');
import fs = require('../../node_modules/fs-extra/');
import path = require('path');
export class Markdown extends React.Component {
private template_filename: string = path.resolve(__dirname + '/../markdown-template');
private template = null;
private text: string = '';
private html: string = '';
static propTypes = { path: React.PropTypes.string }; // this.propsの型定義
state = { style: 'miniframe' }; // this.stateの初期化。getInitialStateはもう不要。
constructor(props){
super(props);
this.template = require(this.template_filename);
this.text = fs.readFileSync(this.props.path, 'utf8');
this.html = marked(this.text);
}
onClick(){
var style = this.state.style === 'miniframe' ? 'maxframe' : 'miniframe';
this.setState({style: style});
}
render(){
return this.template({
html: this.html,
onClick: this.onClick.bind(this) // bindしないとthisが変になる。
});
}
}
一応、ここで使っている、テンプレートファイル(React-jade)とスタイルシート(css)ものせておく。
div(class=this.state.style onClick=onClick dangerouslySetInnerHTML={__html: html})
.maxframe {
background: #F8F8F8;
border: 1px solid #979797;
border-radius: 3px;
width: 80%;
height: 100%;
}
/* Markdown mini frame */
.miniframe {
background: #F8F8F8;
border: 1px solid #979797;
border-radius: 8px;
width: 300px;
height: 300px;
}
/* Title: */
h1 {
font-family: AvenirNext-Bold;
font-size: 30px;
color: #4A4A4A;
line-height: 41px;
}
/* Content: */
p {
font-family: HiraKakuProN-W3;
font-size: 20px;
color: #4A4A4A;
line-height: 24px;
}
index.htmlとかはのせていない。
上記のMarkdownコンポーネントを再利用したいとする。
そんなとき、どうするか。
継承(?)されたコンポーネント
以下のようなクラスを作成する。
class ChildMarkdown extends React.Component {
render(){
return (<Markdown path={filename} />);
}
}
class Markdownを直接継承はしていない。
renderメソッドの返り値に
<Markdown path={filename} />
として、JSX(ここではTypeScriptなのでTSX)で記述しているように、Markdownタグのpathプロパティにファイル名を渡している。
ファイル名を変えることにより別のファイルを使うMarkdownコンポーネントの子クラス(?)を作ることはできる。
ここで、ChildMarkdownクラス(あるいは、そもそものMarkdownクラス)のコンストラクタにファイル名を渡せばすむのではないかという疑問がわくと思う。
だが、それは難しい。
React.Componentの子クラスのconstructorの第1引数にはプロパティ(this.propsという名前でReactでよく出てくるやつにセットされる値)が渡されることになっているからだ。
なんとかならないかといろいろやったが、なんともならなかった…(いい案があれば教えて下さい)。
だが、ChildMarkdownクラスみたいなものを必要な数だけ作るのは、面倒だしダメなパターンにはまっている気がする…。
そこで次の手。
Factoryメソッド
MarkdownクラスにstaticなFactoryメソッドを作ればいい。
export class Markdown extends React.Component {
// 略
static create(filename: string){
const tmpClass = class extends React.Component { // 無名クラス
render(){
return (<Markdown path={filename} />);
}
};
return tmpClass;
}
}
上記のようなメソッドを書いてやれば、あとは使いたいところで、
const component = Markdown.create(filename);
としてコンポーネントを作成し、あとは
const element = React.createElement(component);
const container = document.getElementById('container');
ReactDOM.render(element, container);
みたいな感じでレンダリングしてやればいい。
補足
この記事は、あくまで考え方の説明なので、ビルドに必要なすべてのコードを載せていません。
(いずれ、ソースコードは「7行で作るElectronアプリ」のElecTSのサンプルプロジェクトとして追加する予定です。)
と、ここまで書いといて、すでに誰かがいろんなパターンを考えてそうな予感が…
追記 ー もっと簡単な方法
夕方に届いた「入門 React」を読んでいたら、もっと簡単な方法がありました…
React.createElementの引数にプロパティを渡せるんですね…。
それを使うと、
const element = React.createElement(Markdown, {path: filename});
のように、ファイル名が違うぐらいのものなら、いくつでも作成可能のようです。