ここでの手順はMacOSXでHomebrewがインストール済みであると仮定しています。
Homebrewが入ってない方は、Homebrewインストール手順を参考に入れておいてください。
WindowsやLinuxは適当に... Linuxならnode入れたらあとは同じだろうけど、Windowsはわかりません。。
nodeとcreate-react-appのインストール
$ brew install node
$ sudo npm install -g create-react-app
react.jsプロジェクトを作る
プロジェクト名を timeline
として作る。
$ create-react-app timeline
$ cd timeline
依存ライブラリのインストール
後々必要になるだろうから、サーバーと通信するために react-fetch
と redux-thunk
もインストールしておく。
$ npm install --save react-fetch redux-thunk
あと、後で使うのでルーティングライブラリも入れておく
$ npm install --save react-router react-router-dom
それと最後にReduxで状態管理するようにするので、 redux
と react-redux
も入れておく
$ npm install --save redux react-redux
あ、それと型チェックライブラリの prop-types
も。
$ npm install --save prop-types
nodeサーバーの起動
$ npm start
ブラウザにフォーカスが移って画面が表示されるのを確認。
App.jsの編集
生成直後のApp.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
ここに、入力用のテキストエリアを追加。さらに入力した内容がリアルタイムにテキストエリアの下に表示されるようにする。
-
constructor(props)
を定義して、this.state
としてstate変数を定義する。 -
render()
を変更して、テキストボックスを配置。this.state.text
の内容を常に反映するようにする。 - また、テキストボックスに
onChange
を定義して、内容を変更された時にthis.setState
を呼んで、state変数の内容を変更する。
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: ""
};
}
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
今何してる?
<input type="text" value={ this.state.text }
onChange={e => {this.setState({text: e.target.value})}}/>
</p>
<p>{this.state.text}</p>
</div>
);
}
}
export default App;
さらに、ツイートするとタイムラインに追加されるようにします。
-
constructor
内で、this.state
にtimeline
という配列を入れる変数を追加します。 -
<input type="button">
を追加して、クリックされた場合にthis.state.timeline
にthis.state.text
のテキストを(先頭に)追加。即座にタイムラインに反映される。
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
timeline: []
};
}
render() {
var tweets = [];
for (var i in this.state.timeline) {
tweets.push(<li>{this.state.timeline[i]}</li>)
}
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
今何してる?
<input type="text" value={ this.state.text }
onChange={e => {this.setState({text: e.target.value})}}/>
</p>
<p>
{this.state.text}
<input type="button" value="ツイート" onClick={e => {
var array = this.state.timeline;
array.unshift(this.state.text);
this.setState({timeline: array});
this.setState({text: ""});
}} />
</p>
<ul>{tweets}</ul>
</div>
);
}
}
export default App;
Timelineの部分を別ファイルに分割する
App.js
と同じ階層に Timeline.js
を新たに作る。
内容な以下のような感じ。
ポイントとしては、親(App.js)が持っているtimeline配列をpropsで受け取るようにしているところ。
import React, { Component } from 'react';
class Timeline extends Component {
constructor(props) {
super(props);
this.state = {
timeline: props.timeline
}
}
render() {
let tweets = [];
for (let i in this.state.timeline) {
tweets.push(<li>{this.state.timeline[i]}</li>)
}
return <ul>{tweets}</ul>
}
}
export default Timeline;
App.js
も少し変更.
-
import Timeline from './Timeline.js'
を追加して、上のTimeline.js
を読み込む. - render()でタイムラインを描画していた部分を
<Timeline timeline={this.state.timeline} />
にして、先ほど定義したTimeline
を呼び出すようにする。 -
Timeline
を呼び出すタグでtimeline={this.state.timeline}
としてtimeline配列を渡し、受け取ったTimeline.js
内ではprops.timeline
として使用する。
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Timeline from './Timeline.js'
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
timeline: []
};
}
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
今何してる?
<input type="text" value={ this.state.text }
onChange={e => {this.setState({text: e.target.value})}}/>
</p>
<p>
{this.state.text}
<input type="button" value="ツイート" onClick={e => {
var array = this.state.timeline;
array.unshift(this.state.text);
this.setState({timeline: array});
this.setState({text: ""});
}} />
</p>
<Timeline timeline={this.state.timeline} />
</div>
);
}
}
export default App;
react-routerによるルート制御
さて、今度はこれを、URLによって表示されるページが変わるようにします。
目標としては、 http://localhost:3000/
にアクセスされた場合は先ほどの App
で実装されたページを表示し、 http://localhost:3000/about/(なんらかの数字)
が表示された場合には、このサイトの説明が表示されるページへジャンプします。
index.jsの編集
こちらが変更前。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
そして変更後がこちら。
-
import { BrowserRouter } from 'react-router-dom';
としてBrowserRouterを読み込む。 - ReactDOM.renderで、
<BrowserRouter><App /></BrowserRouter>
としてブラウザ実行時のルーター内にApp
を配置する.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render((
<BrowserRouter>
<App />
</BrowserRouter>
), document.getElementById('root'));
registerServiceWorker();
App.jsの変更
続いて、 App.js
を編集して、App.js内では <Header />
と <Main />
の呼び出しのみとします。
(HeaderとMainは後で作ります)
import React, { Component } from 'react';
import './App.css';
import Header from './Header.js';
import Main from './Main.js';
class App extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
timeline: []
};
}
render() {
return (
<div className="App">
<Header />
<Main />
</div>
);
}
}
export default App;
Header.jsの新規作成
この App.js
内から呼び出される Header.js
と Main.js
を作ります。
まずは Header.js
。 内容は、ページの遷移に関係なく表示する、ページ上部のロゴと、各ページへのリンクです。
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class Header extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2 style={{marginTop: "4px"}}>Welcome to React</h2>
<a href="/hello">ご挨拶</a>:
<a href="/about/1">1について/1</a>:
<a href="/about/2">2について/2</a>:
<a href="/">タイムライン</a>:
</div>
);
}
}
export default Header;
Main.jsの新規作成
続いて Main.js
。こちらは、ここからさらに、呼び出しURLに応じて表示する内容を変えます。
なお、この実装で重要なところは、 render()
関数内で、 <Switch>
を使ってルーティングを制御しているところです。
それと、 About
へのルーティングのところで、pathを /about/:number
としています。このように :(コロン)
を先頭につけると、URL呼び出し時にその位置に指定された値をコンポーネント内で受け取ることができます。
たとえば下記のソースの場合、 Abount.js
内で this.props.match.params.number
として :number
の位置で指定された値を受け取ることができます。
import React, { Component } from 'react';
import './App.css';
import { Switch, Route } from 'react-router-dom';
import TimelineApp from './TimelineApp.js';
import About from './About.js';
import Hello from './Hello.js';
class Main extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
timeline: []
};
}
render() {
return (
<Switch>
<Route exact path='/' component={TimelineApp} />
<Route exact path='/hello' component={Hello} />
<Route exact path='/about/:number' component={About} />
</Switch>
);
}
}
export default Main;
TimelineApp.jsの新規作成
さて、ここで先ほど Main.js
で指定された TimelineApp.js
を実装します。
import React, { Component } from 'react';
import './App.css';
import Timeline from './Timeline.js'
class TimelineApp extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
timeline: []
};
}
render() {
return (
<div className="Main">
<p className="App-intro">
今何してる?
<input type="text" value={ this.state.text }
onChange={e => {this.setState({text: e.target.value})}}/>
</p>
<p>
{this.state.text}
<input type="button" value="ツイート" onClick={e => {
var array = this.state.timeline;
array.unshift(this.state.text);
this.setState({timeline: array});
this.setState({text: ""});
}} />
</p>
<Timeline timeline={this.state.timeline} />
</div>
);
}
}
export default TimelineApp;
Hello.jsの新規作成
さらに、内容はまったく無いのですが、 Hello.js
というコンポーネントを実装して /hello
をブラウザで開いたら上記タイムラインとは違うページが開くようにします。
import React, { Component } from 'react';
import './App.css';
class Hello extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
return (
<div className="Hello">
<h1>ここはHello</h1>
<p>うまくルーティングできたかな?</p>
</div>
);
}
}
export default Hello;
About.jsの新規作成
そして About.js
を新規で作成します。これが Hello.js
と異なるところは、 /abount/3
のようにURLにパラメータがあるとそれを受け取ってページ内に表示するところです。
import React, { Component } from 'react';
import './App.css';
class About extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="App">
<p className="App-intro">
このアプリケーションはReactの勉強用に作りました。<br /><br />
ちなみに渡された値は{ this.props.match.params.number }
</p>
</div>
);
}
}
export default About;
babelによる互換性
なお、 react-create-app
でアプリケーションを作ると、ECMAScript+JSXで書いたアプリが npm run build
時に自動的にIEにも互換性のあるJSで書き出されます(書き出し先はbuildディレクトリ内)。
ただ、 npm start
で起動した場合はこのビルドが行われませんので、 serve
をインストールして、そちらでサーバーを起動し、JSにコンパイルされたソースを使ってアプリを実行します。
$ npm run build
$ npm install -g serve (serveをインストール)
$ serve -s build (buildディレクトリをrootディレクトリとしてサーバーを起動)
┌───────────────────────────────────────────────────┐
│ │
│ Serving! │
│ │
│ - Local: http://localhost:5000 │
│ - On Your Network: http://xxx.xx.xx.xxx:5000 │
│ │
│ Copied local address to clipboard! │
│ │
└───────────────────────────────────────────────────┘
ブラウザでhttp://localhost:5000にアクセス。
この時点でのソースコードはhiroeorz/timelineのrouterブランチにあります。
Reduxの導入
これまでは、それぞれのコンポーネントに state
をもたせて、 this.setState()
として保持している値を更新したりしてきましたが、ここでFluxの考え方を導入します。
Fluxの考え方については私がごちゃごちゃいうより解りやすい記事があるのでそちらを参照してください。
Fluxとはなんなのか
Flux自体は考え方なのですが、このFluxの考え方をもとに作られたReduxというフレームワーク(?)があるので、これを使います。
結局FluxやらReduxやらって何なのか個人的なまとめ
reducerの実装
まずは以下のようにreducerを実装します。
const initialState = {
timeline: [],
text: ""
}
export default function reducer(state = initialState, action) {
switch(action.type) {
/* ツイートを投稿する */
case 'POST_TWEET':
return {
...state,
text: "",
timeline: [action.text, ...state.timeline]
};
/* ツイートを書き込むテキストボックスの内容を保持する */
case 'CHANGE_TEXT':
return {
...state,
text: action.text
};
default:
return state
}
}
内容は、状態保持する state
と引数として渡された action
に応じて分岐して処理するものです。
actionには、この後実装するactionで必ず type
をキーとして渡すことになっており、これを元にどのような処理を行うのかを記述します。で、ここで大切なのは、 state
の内容を更新したい場合、 state
を更新するのではなく、必ず新たに生成した state
を返すということです。
ここでちょっと変わった事をしています。というか、多分フロントをやっている人には常識なんでしょうけど、配列に要素を追加するのに [action.text, ...state.timeline]
としており、これはspread syntaxという書き方なのだそうです。この書き方をすると、ある配列、またはオブジェクトに新たな要素を追加することができます。
例として
let array1 = [1, 2, 3];
let array2 = [...array1, 4, 5, 6];
//=> [1, 2, 3, 4, 5, 6]
let obj1 = {name: "hiroe", age: 42};
let obj2 = {...obj1, nickname: "hiropon"};
//=> {name: "hiroe", age: 42, nickname: "hiropon"}
て感じです。で、こいつのいいところは、もとのオブジェクトを変更せずに、あらたなオブジェクトを生成してくれるようで、reduxのreducerにはうってつけのようです。
actionの実装
actionはこんな感じで実装。
export function mapStateToProps(state) {
return state;
}
export function mapDispatchToProps(dispatch) {
return {
/* ツイートを投稿する */
postTweet: (text) => {
dispatch( {type: 'POST_TWEET', text: text} );
},
/* ツイートを書き込むテキストボックスの内容をstate.textに保持する */
changeText: (text) => {
dispatch( {type: 'CHANGE_TEXT', text: text} );
}
}
}
mapStateToProps
と mapDispatchToProps
の2つを実装しています。
mapDispatchToProps内に呼ばれた関数をactionオブジェクトに変換する関数を実装しています。ここでの処理はあくまで、呼ばれた関数をactionオブジェクトに変換するだけという点に注意が必要です。
こうしておくと、先ほどのreducerにオブジェクトが渡るので、実際にstateを更新する処理はreducer側で書くことになります。
storeの生成
- index.jsを編集し、このアプリケーション全体で使用する
store
を生成します(const store = createStore(reducer);
)。 - さらに、
Provider
タグでレンダリングする全体を囲んで、アプリケーション全体でstore
を使用できるようにします。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { BrowserRouter } from 'react-router-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from './reducer.js';
const store = createStore(reducer);
ReactDOM.render((
<Provider store={store} >
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
), document.getElementById('root'));
registerServiceWorker();
connectによるReactとReduxの接続
最後に、connectによってReactとReduxを接続します。
やることは、Reduxによって管理されている state
から値を取得したり値を更新するコンポーネントで、connect関数によって先ほどのactionとコンポーネントのつなぎこみをします。
ソースは以下のようになりました。
Timeline.jsの編集
- 今まで
export default Timeline
としていたところをexport default connect(mapStateToProps)(Timeline)
に変更しています(mapStateToProps
はsrc/action.js
内で実装しています。) - また、stateの初期化が必要なくなったため、constructorを削除しています。そのほかの
App.js
やAbout.js
等、ほかのコンポーネントにあったconstructorもすべて削除しました。reduxを使う場合、状態の保持はstoreで行われるので各コンポーネントでのstateの定義は必要なくなります。
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { mapStateToProps } from './action.js';
class Timeline extends Component {
render() {
let tweets = [];
for (let i in this.props.timeline) {
tweets.push(<li key={i}>{this.props.timeline[i]}</li>)
}
return <ul>{tweets}</ul>
}
}
export default connect(mapStateToProps)(Timeline);
TimelineApp.jsの編集
こちらも同様に connect
を使ってactionとコンポーネントを繋いでいます。
先ほどの Timeline.js
と異なる点は、繋ぎ込みの際に、 mapDispatchToProps
も繋いでいる点です。
これは、 Timeline.js
ではstateの値の参照だけだったのに対して、 TimelineApp.js
では state
に保存された値を更新するための関数を呼び出すからです。
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './App.css';
import Timeline from './Timeline.js'
import { mapStateToProps, mapDispatchToProps } from './action.js';
class TimelineApp extends Component {
render() {
return (
<div className="Main">
<p className="App-intro">
今何してる?
<input type="text" value={ this.props.text }
onChange={ (e) => this.props.changeText(e.target.value)} />
</p>
<p>
{this.props.text}
<button onClick={ () => this.props.postTweet(this.props.text) }>
つぶやく
</button>
</p>
<Timeline timeline={this.props.timeline} />
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TimelineApp);
以上で完了です。ブラウザで http://localhost:3000にアクセスして動作確認します。
reduxを使うことで、状態管理を一箇所にあつめることができ、各コンポーネントで管理する必要がないのでソースがすっきりしました。
この時点でのソースコードはhiroeorz/timelineのreduxブランチにあります。
PropTypesで型チェック
reactでは、 PropTypes
というライブラリを使って、propsとして外部から渡されるオブジェクトの型チェックを行うことができます。
構文は、例えば App
コンポーネントにに渡されるpropsの型チェックの場合は
App.propTypes = {
text: PropTypes.string,
timeline: PropTypes.arrayOf(PropTypes.string)
}
てな具合です。2つめの timeline
は、文字列の配列なので、 PropTypes.string
を PropTypes.arrayOf()
で囲っています。
さて、ここまでのコードにproptypesを適用すると、propsから値を受け取って何かするのは Timeline
と TimelineApp
の2つのコンポーネントですのでここで定義します。
まずは Timeline.js
から。
最初に import PropTypes from 'prop-types';
としてライブラリをインポートし、後半で TimelineApp.propTypes = {...}
として TimelineApp
コンポーネントで使用するプロパティの型チェックをおこなっています。
また、必須のプロパティについては isRequired
をつけておきます。あってもなくてもいいなら必要ないです。
PropTypesの詳しい使い方についてはTypechecking With PropTypesに詳しく書いてあります。
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import './App.css';
import Timeline from './Timeline.js'
import { mapStateToProps, mapDispatchToProps } from './action.js';
class TimelineApp extends Component {
render() {
return (
<div className="Main">
<p className="App-intro">
今何してる?
<input type="text" value={ this.props.text }
onChange={ (e) => this.props.changeText(e.target.value)} />
</p>
<p>
{this.props.text}
<button onClick={ () => this.props.postTweet(this.props.text) }>
つぶやく
</button>
</p>
<Timeline timeline={this.props.timeline} />
</div>
);
}
}
TimelineApp.propTypes = {
text: PropTypes.string.isRequired,
timeline: PropTypes.arrayOf(PropTypes.string).isRequired
}
export default connect(mapStateToProps, mapDispatchToProps)(TimelineApp);
同様に Timeline
コンポーネント。
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { mapStateToProps } from './action.js';
class Timeline extends Component {
render() {
let tweets = [];
for (let i in this.props.timeline) {
tweets.push(<li key={i}>{this.props.timeline[i]}</li>)
}
return <ul>{tweets}</ul>
}
}
Timeline.propTypes = {
timeline: PropTypes.arrayOf(PropTypes.string).isRequired
}
export default connect(mapStateToProps)(Timeline);
こちらは props.timeline
しか使わないので、これだけ型チェック。
なお、型チェックに失敗してもユーザーにはエラーが表示されず、デベロッパーツールで以下のように警告されます。
fetch + redux-thunk によるHTTP API連携
さて、reduxの導入、PropTypesの型検査まできましたが、最後にWebアプリでは必須と言えるHTTP API連携の実装をします。
調査したところでは、いくつかの方法があるようですが、私が見た中で最も分かりやすかった fetch
と redux-thunk
を使った方法で実装して見ます。
fetchはreact標準のHTTPクライアントのようで、基本的な使い方は
fetch('http://localhost:3000/hiroe.json').then(
response => {
response.json().then(
(json) => dispatch({type: 'HUMAN', human: json})
)
},
error =>
dispatch({type: 'HUMAN_ERROR', reason: error})
)
て感じになるようです。fetchに引数としてURLを渡し、その戻りに対する処理内容を成功の場合と失敗の場合で分けて関数で渡します。
で、redux-thunkがなにかというと、githubのREADMEからMotivationをGoogleさんに和訳してもらうと
Redux Thunkミドルウェアを使用すると、アクションではなく関数を返すアクションクリエータを作成できます。
thunkは、アクションのディスパッチを遅らせるために、または特定の条件が満たされた場合にのみディスパッチ
するために使用できます。
内部関数は、ストア・メソッドdispatchおよびgetStateをパラメータとして受け取ります。
というわけで、thunkを使うとactionクリエーターの戻り値に、処理した結果ではなく、処理内容を包んだ関数を返すようにし、これを非同期で実行できるようになるようです。
なので、fetchとthunkを組み合わせることで、fetchでHTTP APIを叩き、非同期にデータを受け取ったらjsonパースしてディスパッチする、というようなことが実現するようです。
なにはともあれ作ってみます。
HTTP APIは通常はnodeやRailsなどで動的な値を返すサーバーを組むでしょうが、だるいので今回は以下のようなファイルをプロジェクトの public/hiroe.json
として新規作成して内容が固定のHTTP APIとして使用します。
{
"name": "ひろえ",
"age": 41,
"nickname": "ひろぽん"
}
これで、http://localhost:3000/hiroe.jsonにブラウザでアクセスすると、このファイルの内容が表示されます。
また、先ほどまでは store
を src/index.js
内で定義していましたが、これを store.js
という別ファイルに移し、 index.js
と action.js
の両方から使えるようにします。
また、storeを作る際に、引数に applyMiddleware(thunk)
として thunk
をミドルウエアとして指定しています。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer.js';
export const store = createStore(reducer, applyMiddleware(thunk));
これを src/index.js
から読み込むように src/index.js
を編集します。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './store.js';
ReactDOM.render((
<Provider store={store} >
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
), document.getElementById('root'));
registerServiceWorker();
さて、今回は新たに src/Human.js
というコンポーネントを作って、「ボタンを押したらサーバーから取得した内容を表示する」ということをします。
まずは以下のように src/Human.js
を作成します。
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import './App.css';
import { mapStateToProps, mapDispatchToProps } from './action.js';
class Human extends Component {
render() {
return (
<div className="Main">
<button onClick={ () => this.props.searchHuman() }>表示</button>
<table>
<tbody>
<tr>
<th>名前</th> <td>{ this.props.human.name }</td>
</tr>
<tr>
<th>年齢</th> <td>{ this.props.human.age }歳</td>
</tr>
<tr>
<th>ニックネーム</th> <td>{ this.props.human.nickname }</td>
</tr>
</tbody>
</table>
</div>
);
}
}
Human.propTypes = {
human: PropTypes.shape({
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
nickname: PropTypes.string.isRequired
}).isRequired
}
export default connect(mapStateToProps, mapDispatchToProps)(Human);
「表示」ボタンをクリックすると、サーバーから取得した内容が props
に渡され、これをテーブルに表示します。
続いて、各ページへのルーティングを記述していた src/Main.js
にて、 /human
にアクセスがあったら Human
をコンポーネントとしてページを表示するようにルートを追加します。
import React, { Component } from 'react';
import './App.css';
import { Switch, Route } from 'react-router-dom';
import TimelineApp from './TimelineApp.js';
import About from './About.js';
import Hello from './Hello.js';
import Human from './Human.js';
class Main extends Component {
render() {
return (
<Switch>
<Route exact path='/' component={TimelineApp} />
<Route exact path='/about/:number' component={About} />
<Route exact path='/hello' component={Hello} />
<Route exact path='/human' component={Human} />
</Switch>
);
}
}
export default Main;
ルートの最後に Human
を追加しています。
次に、ヘッダ部にリンクを追加します。修正するのは src/Header.js
です。
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class Header extends Component {
render() {
return (
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2 style={{marginTop: "4px"}}>Welcome to React</h2>
<a href="/hello">ご挨拶</a>:
<a href="/about/1">1について/1</a>:
<a href="/about/2">2について/2</a>:
<a href="/">タイムライン</a>:
<a href="/human">人間検索</a>:
</div>
);
}
}
export default Header;
さて、続いて src/action.js
を編集し、先ほどの src/Human.js
においてボタンに設定したアクション( onClick={ () => this.props.searchHuman() }
)を実装します。
import {store} from './store.js';
export function mapStateToProps(state) {
return state;
}
export function mapDispatchToProps(dispatch) {
return {
/* ツイートを投稿する */
postTweet: (text) => {
dispatch( {type: 'POST_TWEET', text: text} );
},
/* ツイートを書き込むテキストボックスの内容をstate.textに保持する */
changeText: (text) => {
dispatch( {type: 'CHANGE_TEXT', text: text} );
},
/* サーバーからJSONを取得し、その内容を表示する */
searchHuman: () => {
let f = function (dispatch) {
return fetch('http://localhost:3000/hiroe.json').then(
response => {
response.json().then(
(json) => dispatch({type: 'HUMAN', human: json})
)
},
error =>
dispatch({type: 'HUMAN_ERROR', reason: error})
)
}
store.dispatch(f).then( () => { console.log("Done!"); })
}
}
}
追加したのは searchHuman
という関数です。
ここで「HTTP APIをfetchし、取得したJSONをパースし、最後にその内容をdispatchに渡す処理」を記述した関数を store.dispatch(f)
として渡しておき、完了したらコンソールに Done!
と表示するようにしています。
続いて src/reducer.js
を編集します。
const initialState = {
timeline: [],
text: "",
human: {name: "", age: 0, nickname: ""}
}
export default function reducer(state = initialState, action) {
switch(action.type) {
/* ツイートを投稿する */
case 'POST_TWEET':
console.log(state);
return {
...state,
text: "",
timeline: [action.text, ...state.timeline]
};
/* ツイートを書き込むテキストボックスの内容を保持する */
case 'CHANGE_TEXT':
return {
...state,
text: action.text
};
/* サーバーからJSONを取得し、その内容を表示する */
case 'HUMAN':
console.log(action);
return {
...state,
human: action.human
};
/* サーバーからJSONを取得する時に発生したエラー情報をコンソールに表示する */
case 'HUMAN_ERROR':
console.log(action);
return state;
default:
return state
}
}
アクションタイプの分岐に HUMAN
と HUMAN_ERROR
を追加しています。
さて、これで http://localhost:3000/humanにブラウザでアクセスすると「表示」ボタンが表示されており、ボタンをクリックするとサーバーから取得した内容が表示されます。
とりあえずここまで。
初心者なので色々勘違いがあるかもしれません。指摘歓迎しますのでよろしくお願いします。