はじめに
Reduxのチュートリアルをほんのちょっといじれば今自分が作りたいBlog機能を作成できるのでは?ということでそのプロトタイプを作成した。ほとんどTodoと変わらないが、編集機能の部分について工夫を入れている。
サンプルコード
機能の特徴
- 記事一覧と、記事作成を同一のスクリーンで行う
- 記事のタイトルと記事内容を書いて、ボタンをクリックすると記事一覧にそのボタンが作成した順に並ぶ(作成)
- 記事一覧の任意の記事をクリックすると、記事作成欄にその記事のタイトルと記事内容を表示する(編集)
- 記事一覧にある、各記事の[x]をクリックすると、その記事が消える(削除)
- 簡単のため、バリデーション処理は実装していない
気づき
input要素にprops
のデータを渡す時の注意
- 記事一覧の任意の記事をクリックすると、記事作成欄にその記事のタイトルと記事内容を表示する(編集)
この機能を実現しようとすると、一連のフローは次のようになる。
[User Interaction(Click an Entry)] -> [Action(Modify)] -> [Store(CurrentEntry)] -> [Component]
この時、[Store(CurrentEntry)]を使って[Component]のinput要素の値を切り替えることになるが、そのやり方を発見するのにハマった。
render()
の内部でこの情報を直接埋め込むことは、controlled componentの制約でできない。
<input value={this.props.someVale} />
そこで内部ステータス1を使って、ライフサイクル関数componentWillReceiveProps(nextProps)
の中でその値setState()
して目的を果たしている。
class AddEntry extends Component {
constructor() {
super();
this.state = {
title: '',
content: ''
};
}
componentDidMount() {
// console.log('Add entry component Did mount');
}
componentWillReceiveProps(nextProp) {
// console.log('componentWillReceiveProps')
const { entry } = nextProp;
this.setState({
title: entry.title,
content: entry.content
})
}
render() {
return (
<div>
<h2>Add Entry</h2>
<form>
<label>title</label><br />
<input
ref={node => this.titleInput = node}
value={this.state.title || ''}
onChange={e => {
this.setState({title: e.target.value});
}}
type="text" /><br />
<label>content</label><br />
<textarea
rows={10}
cols={50}
ref={node => this.contentInput = node}
value={this.state.content || ''}
onChange={e => {
this.setState({content: e.target.value});
}}
/><br />
<button
onClick={(e)=>{
e.preventDefault();
store.dispatch(
addEntry(
this.state.title,
this.state.content
)
);
this.setState({
content: '',
title: ''
});
}}>
add Entry
</button>
</form>
</div>
);
}
}
connect()するとコンポーネントにstore情報は渡らない?
connect()
するとmapStateToProps()
で、ある時点のstoreの中身を編集しprops
の一部として渡す値を指定できる。ただ、そんなに手の込んだ編集がない場合は、わざわざmapStateToProps()
を定義しなくてもstoreの中身を取得するための代替案はないかな?と思ったが良い方法はなかった。
// it doesn't work!!
const Entry = ({
// entries,
handleClick,
handleDeleteClick
},{
{ entries } // props から受け渡されなくても、 contextから取得できるとか・・
}
) => (
<div>
<div onClick={handleClick}>
<span>{entry.title}</span>
<br />
<div
dangerouslySetInnerHTML={{
__html: entry.content.replace(/\n/g, '<br />')
}} />
<span>{entry.createdAt ? entry.createdAt : 'no date'}</span>
</div>
<a onClick={handleDeleteClick}>[x]</a>
<hr />
</div>
);
/*
* Conatainer Component (CurrentEntry)
*
const mapStateToEntryProps = (
state,
ownProps
) => {
return {
entry: ownProps.entry
};
};
*/
const mapDispatchToEntryProps = (
dispatch,
ownProps
) => {
return {
handleClick() {
dispatch({
type: ActionType.SELECT_ENTRY,
entry: ownProps.entry
});
},
handleDeleteClick() {
dispatch(deleteEntry(ownProps.entry.id));
}
};
};
const CurrentEntry = connect(
null,
mapDispatchToEntryProps
)(Entry);
終わりに
機能的な確認ができただけでなく、プレゼンテーショナルコンポーネントとコンテナコンポーネントをどのように切り分けていくと良いか、という方針をつかむための経験が積めた。
また、この記事には現れていないが、Reactを実装する前に、Reducerのテストが行える点、そしてReducerがコンパクトに分解できるのでテストしやすい点に好感を覚えた。
-
Reduxのチュートリアル のような単にinput要素の値を使ってActionを発生させるだけなら要らない ↩