17
18

More than 5 years have passed since last update.

React.jsのプロジェクトはじめの一歩 〜簡単なタイムラインの実装からRedux導入、そしてHTTP APIまで〜

Last updated at Posted at 2017-08-28

ここでの手順は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-fetchredux-thunk もインストールしておく。

$ npm install --save react-fetch redux-thunk

あと、後で使うのでルーティングライブラリも入れておく

$ npm install --save react-router react-router-dom

それと最後にReduxで状態管理するようにするので、 reduxreact-redux も入れておく

$ npm install --save redux react-redux

あ、それと型チェックライブラリの prop-types も。

$ npm install --save prop-types

nodeサーバーの起動

$ npm start

ブラウザにフォーカスが移って画面が表示されるのを確認。

App.jsの編集

生成直後のApp.js

src/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;

ここに、入力用のテキストエリアを追加。さらに入力した内容がリアルタイムにテキストエリアの下に表示されるようにする。

  1. constructor(props) を定義して、 this.state としてstate変数を定義する。
  2. render() を変更して、テキストボックスを配置。 this.state.text の内容を常に反映するようにする。
  3. また、テキストボックスに onChange を定義して、内容を変更された時に this.setState を呼んで、state変数の内容を変更する。
src/App.js
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;

さらに、ツイートするとタイムラインに追加されるようにします。

  1. constructor 内で、 this.statetimeline という配列を入れる変数を追加します。
  2. <input type="button"> を追加して、クリックされた場合に this.state.timelinethis.state.text のテキストを(先頭に)追加。即座にタイムラインに反映される。
src/App.js
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で受け取るようにしているところ。

src/Timeline.js
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 も少し変更.

  1. import Timeline from './Timeline.js' を追加して、上の Timeline.js を読み込む.
  2. render()でタイムラインを描画していた部分を <Timeline timeline={this.state.timeline} />にして、先ほど定義した Timeline を呼び出すようにする。
  3. Timeline を呼び出すタグで timeline={this.state.timeline} としてtimeline配列を渡し、受け取った Timeline.js 内では props.timeline として使用する。
src/App.js
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;

最終的な状態
スクリーンショット 2017-08-29 12.48.19.png

react-routerによるルート制御

さて、今度はこれを、URLによって表示されるページが変わるようにします。
目標としては、 http://localhost:3000/ にアクセスされた場合は先ほどの App で実装されたページを表示し、 http://localhost:3000/about/(なんらかの数字) が表示された場合には、このサイトの説明が表示されるページへジャンプします。

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';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

そして変更後がこちら。

  1. import { BrowserRouter } from 'react-router-dom'; としてBrowserRouterを読み込む。
  2. ReactDOM.renderで、 <BrowserRouter><App /></BrowserRouter> としてブラウザ実行時のルーター内に Appを配置する.
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';

ReactDOM.render((
  <BrowserRouter>
    <App />
  </BrowserRouter>
), document.getElementById('root'));

registerServiceWorker();

App.jsの変更

続いて、 App.js を編集して、App.js内では <Header /><Main /> の呼び出しのみとします。
(HeaderとMainは後で作ります)

src/App.js
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.jsMain.js を作ります。
まずは Header.js 。 内容は、ページの遷移に関係なく表示する、ページ上部のロゴと、各ページへのリンクです。

src/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 の位置で指定された値を受け取ることができます。

src/Main.js
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 を実装します。

src/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 をブラウザで開いたら上記タイムラインとは違うページが開くようにします。

src/Hello.js
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にパラメータがあるとそれを受け取ってページ内に表示するところです。

src/About.js
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を実装します。

src/reducer.js
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はこんな感じで実装。

src/action.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} );
    }
  }
}

mapStateToPropsmapDispatchToProps の2つを実装しています。
mapDispatchToProps内に呼ばれた関数をactionオブジェクトに変換する関数を実装しています。ここでの処理はあくまで、呼ばれた関数をactionオブジェクトに変換するだけという点に注意が必要です。
こうしておくと、先ほどのreducerにオブジェクトが渡るので、実際にstateを更新する処理はreducer側で書くことになります。

storeの生成

  1. index.jsを編集し、このアプリケーション全体で使用する store を生成します( const store = createStore(reducer); )。
  2. さらに、Provider タグでレンダリングする全体を囲んで、アプリケーション全体で store を使用できるようにします。
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 { 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の編集

  1. 今まで export default Timeline としていたところを export default connect(mapStateToProps)(Timeline) に変更しています( mapStateToPropssrc/action.js 内で実装しています。)
  2. また、stateの初期化が必要なくなったため、constructorを削除しています。そのほかの App.jsAbout.js 等、ほかのコンポーネントにあったconstructorもすべて削除しました。reduxを使う場合、状態の保持はstoreで行われるので各コンポーネントでのstateの定義は必要なくなります。
src/Timeline.js
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 に保存された値を更新するための関数を呼び出すからです。

src/TimelineApp.js
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.stringPropTypes.arrayOf() で囲っています。

さて、ここまでのコードにproptypesを適用すると、propsから値を受け取って何かするのは TimelineTimelineApp の2つのコンポーネントですのでここで定義します。

まずは Timeline.js から。

最初に import PropTypes from 'prop-types'; としてライブラリをインポートし、後半で TimelineApp.propTypes = {...} として TimelineApp コンポーネントで使用するプロパティの型チェックをおこなっています。

また、必須のプロパティについては isRequired をつけておきます。あってもなくてもいいなら必要ないです。
PropTypesの詳しい使い方についてはTypechecking With PropTypesに詳しく書いてあります。

src/TimelineApp.js
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 コンポーネント。

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 しか使わないので、これだけ型チェック。

なお、型チェックに失敗してもユーザーにはエラーが表示されず、デベロッパーツールで以下のように警告されます。
スクリーンショット 2017-09-05 22.25.15.png

fetch + redux-thunk によるHTTP API連携

さて、reduxの導入、PropTypesの型検査まできましたが、最後にWebアプリでは必須と言えるHTTP API連携の実装をします。
調査したところでは、いくつかの方法があるようですが、私が見た中で最も分かりやすかった fetchredux-thunk を使った方法で実装して見ます。

fetchはreact標準のHTTPクライアントのようで、基本的な使い方は

fetchの基本的使い方
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として使用します。

public/hiroe.json
{
  "name": "ひろえ",
  "age": 41,
  "nickname": "ひろぽん"
}

これで、http://localhost:3000/hiroe.jsonにブラウザでアクセスすると、このファイルの内容が表示されます。

また、先ほどまでは storesrc/index.js 内で定義していましたが、これを store.js という別ファイルに移し、 index.jsaction.js の両方から使えるようにします。
また、storeを作る際に、引数に applyMiddleware(thunk) として thunk をミドルウエアとして指定しています。

src/store.js
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 を編集します。

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 を作成します。

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 をコンポーネントとしてページを表示するようにルートを追加します。

src/Main.js
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 です。

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() } )を実装します。

src/action.js
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 を編集します。

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
  }
}

アクションタイプの分岐に HUMANHUMAN_ERROR を追加しています。
さて、これで http://localhost:3000/humanにブラウザでアクセスすると「表示」ボタンが表示されており、ボタンをクリックするとサーバーから取得した内容が表示されます。

スクリーンショット 2017-09-07 9.57.41.png

とりあえずここまで。
初心者なので色々勘違いがあるかもしれません。指摘歓迎しますのでよろしくお願いします。

17
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
18