Help us understand the problem. What is going on with this article?

React.js tutorial

More than 1 year has passed since last update.

SPA したくて React.js を始めたい人に手っ取り早く動かせるように tutorial を作りました。学習コストを下げるために redux を避けているので割ととっつきやすいと思います。たぶん。

この記事にあるソースコードはこちらに置いてあります。

https://github.com/okamuuu/fav-articles

STEP0: 準備

この tutorial では create-react-app コマンドを使うのでこれを最初に install します。

npm install -g create-react-app

このコマンドを使ってプロジェクトフィアル一式を生成します。

create-react-app fav-articles && cd $_

STEP1: Create fake server

json-server はとても便利なライブラリです。faker もなかなか便利なのでこれらを使って mock server を作ります。

npm install --save json-server faker
touch server.js

create server.js

const faker = require('faker')
const jsonServer = require('json-server')
const server = jsonServer.create()

server.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "http://localhost:3000")
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
  next()
})

const rewriter = jsonServer.rewriter({'/api/*': '/$1'})
const router = jsonServer.router(getArticles())

server.use(rewriter)
server.use(router)

function getArticles() {
  const articles = []

  for (var id = 1; id < 51; id++) {

    articles.push({
      "id": id,
      "title": faker.lorem.words(),
      "description": faker.lorem.paragraphs(),
      "isFavorite": false
    })
  }

  return { "articles": articles }
}

module.exports = server

edit package.json

     "start": "react-scripts start",
     "build": "react-scripts build",
     "test": "react-scripts test --env=jsdom",
-    "eject": "react-scripts eject"
+    "eject": "react-scripts eject",
+    "serve": "node -e \"require('./server').listen(4000)\""
   }

動作確認

npm run serve

> practice-app@0.1.0 serve /Users/okamuuu/practice-app
> node -e "require('./server').listen(4000)"

50件の記事が表示されることを確認します。

open http://localhost:4000/api/articles

STEP2: Create Api class and the test case

WEB API との通信には axiosparse-link-header を使います。

npm install --save axios parse-link-header
touch src/Api.js src/Api.test.js

create src/Api.js

import axios from 'axios'
import httpAdapter from 'axios/lib/adapters/http'
import parse from 'parse-link-header'

// https://github.com/mzabriskie/axios/issues/305#issuecomment-233141731
// for jest. force the node adapter
if (process.env.NODE_ENV == "test") {
  axios.defaults.adapter = httpAdapter
}

export default class Api {

  constructor(baseUrl) {
    this.baseUrl = baseUrl
  }

  listArticles(page=1) {
    return axios.get(`${this.baseUrl}/api/articles?_page=${page}`).then((res) => {
      return {
        "articles": res.data || [], 
        "links": parse(res.headers.link)
      }   
    })  
  }

  listFavoriteArticles(page=1) {
    return axios.get(`${this.baseUrl}/api/articles?isFavorite=true&_page=${page}&_limit=50`).then((res) => {
      return {
        "articles": res.data || [], 
        "links": parse(res.headers.link)
      }   
    })  
  }

  showArticle(id) {
    return axios.get(`${this.baseUrl}/api/articles/${id}`).then((res) => {
      return { "article": res.data }
   })
  }

  updateArticle(id, params) {
    return axios.put(`${this.baseUrl}/api/articles/${id}`, params).then((res) => {
      return { "article": res.data }
    })
  }
}

create src/Api.test.js

import server from '../server'
import Api from '../src/Api'

const port = server.listen(0).address().port
const api = new Api(`http://127.0.0.1:${port}`)

describe('Api', function() {

  test('listArticles', async () => {
    const result = await api.listArticles()
    expect(result.articles.length).toEqual(10)
  })  
})

CI=true npm run test を実行すると単体テストができます。

% CI=true npm test

> fav-articles@0.1.0 test /Users/okamuuu/fav-articles
> react-scripts test --env=jsdom

 PASS  src/Api.test.js
 PASS  src/App.test.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.843s
Ran all test suites.

STEP3: A few Designing

bootstrap のデザインをつかいます。

npm install --save bootstrap

edit src/index.js

 import React from 'react';
 import ReactDOM from 'react-dom';
 import App from './App';
+import 'bootstrap/dist/css/bootstrap.css'
 import './index.css';

このやり方だと css だけしか使えないのですが今回は Bootstrap の JavaScript を使わないのでここまででOKです。もし Bootstrap の JavaScript を使いたい場合は下記の記事を参考にして下さい。

http://okamuuu.hatenablog.com/entry/2016/07/18/115404

STEP4: react-router

react-router を使用します。最近 v3 から v4 に変更されています。

npm install --save react-router-dom
touch src/Articles.js

create src/Articles.js

import React, {Component} from 'react'
import { Link } from 'react-router-dom'

class List extends Component {

  render() {
    return (
      <div>
        <h2>Articles</h2>
        <ul>
          <li><Link to="/articles/1">Show 1</Link></li>
          <li><Link to="/articles/2">Show 2</Link></li>
          <li><Link to="/articles/3">Show 3</Link></li>
        </ul>
      </div>
    )
  }
}

class FavoriteList extends Component {

  render() {
    return (
      <div>
        <h2>Favorite Articles</h2>
      </div>
    )
  }
}

class Show extends Component {

  render() {
    return (
      <div>
        <h3>Show {this.props.match.params.id}</h3>
      </div>
    )
  }
}

export default { List, FavoriteList, Show }

edit src/App.js

import React from 'react'
import { BrowserRouter as Router, Switch, Route, Link, NavLink, withRouter } from 'react-router-dom'

import Articles from './Articles'

const Header = ({onClick}) => (
  <h1 className="text-center" style={{cursor: "pointer" }} onClick={onClick}>Favorite Articles</h1>
)

const Nav = () => (
  <ul className="nav nav-pills">
    <li><NavLink exact to="/articles">Articles</NavLink></li>
    <li><NavLink exact to="/articles/favorite">Favorite Articles</NavLink></li>
  </ul>
)

const Footer = () => (<p className="text-center">Favorite Articles</p>)

const Routes = withRouter(({history}) => (
  <div className="container">
    <Header onClick={() => history.push("/")} />
    <Nav />
    <Switch>
      <Route exact path="/" component={Articles.List}/>
      <Route exact path="/articles" component={Articles.List}/>
      <Route exact path="/articles/favorite" component={Articles.FavoriteList}/>
      <Route exact path="/articles/:id" component={Articles.Show}/>
    </Switch>
    <Footer />
  </div>
))

const App = () => (
  <Router>
    <Routes />
  </Router>
)

export default App

npm start して open http://localhost:3000 動作確認をします。

react-router@v4 に関しては下記の記事も参考にしてみてください。

http://okamuuu.hatenablog.com/entry/2017/03/23/140650

STEP5: Using Api Class

最初に作成した Api を組み込みます。

edit src/Articles.js

import React, {Component} from 'react'
import { Link } from 'react-router-dom'

import Api from './Api'

const api = new Api(`http://127.0.0.1:4000`)

class List extends Component {

  constructor(props) {
    super(props)
    this.state = { articles: [] }
  }

  componentWillMount() {
    api.listArticles().then((result) => {
      this.setState({articles: result.articles, current: 1, last: result.links.last._page})
    })
  }

  render() {
    return (
      <div>
        <h2>Articles</h2>
        <ul>
          {this.state.articles.map((x, index) => (
            <li key={index}>
              <Link to={`/articles/${x.id}`}>{x.title}</Link>
            </li>
          ))}
        </ul>
      </div>
    )
  }
}

class FavoriteList extends Component {

  render() {
    return (
      <div>
        <h2>Favorite Articles</h2>
      </div>
    )
  }
}

class Show extends Component {

  constructor(props) {
    super(props)
    this.state = { article: {} }
  }

  componentWillMount() {
    const { id } = this.props.match.params
    api.showArticle(parseInt(id, 10)).then((result) => {
      this.setState({article: result.article})
    })
  }

  render() {
    const {article} = this.state
    return (
      <div>
        <h2>{article.title}</h2>
        <p>{article.description}</p>
      </div>
    )
  }
}

export default { List, FavoriteList, Show }

再度 npm start and open http://localhost:3000 で動作確認をして下さい。
なお、npm run serve が起動していないと WEB API の通信に失敗します。

STEP6: Add Favorite Toggle Button

SVG なデータをアイコンとして使うケースがあると思いますが、 react-icons に主要なアイコンが揃っているのでそれを使います。

npm install --save react-icons immutable

edit src/Articles.js to add FavoriteButton and update FavoriteList function.

import React, {Component} from 'react'
import { Link } from 'react-router-dom'

import FaStar from 'react-icons/lib/fa/star'
import immutable from 'immutable'

import Api from './Api'

const api = new Api(`http://127.0.0.1:4000`)

const FavoriteButton = ({isFavorite, onClick}) => (
  <FaStar style={{cursor: "pointer"}} color={isFavorite ? "#ffa500" : "#eee"} onClick={onClick} />)

class List extends Component {

  constructor(props) {
    super(props)
    this.state = { articles: [] }
  }

  componentWillMount() {
    api.listArticles().then((result) => {
      this.setState({articles: result.articles, current: 1, last: result.links.last._page})
    })
  }

  handleFavorite(article, index) {
    article.isFavorite = article.isFavorite !== true
    api.updateArticle(article.id, article).then((result) => {
      const nextArticles = immutable.List(this.state.articles)
      nextArticles[index] = result.article
      this.setState({articles: nextArticles})
    })  
  }

  render() {
    return (
      <div>
        <h2>Articles</h2>
        <ul>
          {this.state.articles.map((x, index) => (
            <li key={index}>
              <Link to={`/articles/${x.id}`}>{x.title}</Link>
              {" "}
              <FavoriteButton isFavorite={x.isFavorite} onClick={() => this.handleFavorite(x, index)} />
            </li>
          ))}
        </ul>
      </div>
    )
  }
}

class FavoriteList extends Component {

  constructor(props) {
    super(props)
    this.state = { articles: [] }
  }

  componentWillMount() {
    api.listFavoriteArticles().then((result) => {
      this.setState({articles: result.articles})
    })
  }

  render() {

    const {articles} = this.state

    return (
      <div>
        <h2>Favorites</h2>
        <ul>
        {this.state.articles.map((x, index) => (
          <li key={index}><Link to={`/articles/${x.id}`}>{x.title}</Link></li>
        ))}
        </ul>
      </div>
    )
  }
}

class Show extends Component {

  constructor(props) {
    super(props)
    this.state = { article: {} }
  }

  componentWillMount() {
    const { id } = this.props.match.params
    api.showArticle(parseInt(id, 10)).then((result) => {
      this.setState({article: result.article})
    })
  }

  render() {
    const {article} = this.state
    return (
      <div>
        <h2>{article.title}</h2>
        <p>{article.description}</p>
      </div>
    )
  }
}

export default { List, FavoriteList, Show }

もう一度 npm start した状態で open http://localhost:3000 で確認します。もう少しで終わります。

STEP7: Add Pagination

最後に拙作の components を使った Pagination を紹介します。

npm install --save react-paginators

edit src/Articles.js

diff --git a/src/Articles.js b/src/Articles.js
index 0b97cbd..6c87c01 100644
--- a/src/Articles.js
+++ b/src/Articles.js
@@ -5,6 +5,7 @@ import FaStar from 'react-icons/lib/fa/star'
 import immutable from 'immutable'

 import Api from './Api'
+import { Bootstrap3ishPaginator } from 'react-paginators'

 const api = new Api(`http://127.0.0.1:4000`)

@@ -33,7 +34,17 @@ class List extends Component {
     })
   }

+  handlePageClick(page) {
+    api.listArticles(page).then((result) => {
+       this.setState({articles: result.articles, current: page, last: result.links.last._page})
+    })
+  }
+
   render() {
+
+    const current = this.state && this.state.current || 1
+    const last = this.state && parseInt(this.state.last, 10) || 1
+
     return (
       <div>
         <h2>Articles</h2>
@@ -46,6 +57,15 @@ class List extends Component {
             </li>
           ))}
         </ul>
+
+        <div style={{padding: "30px",  display: "flex", justifyContent: "center" }}>
+          <Bootstrap3ishPaginator
+            current={current}
+            last={last}
+            maxPageCount={10}
+            onClick={this.handlePageClick.bind(this)}
+          />
+        </div>
       </div>
     )
   }
@@ -107,3 +127,4 @@ class Show extends Component {
 }

 export default { List, FavoriteList, Show }

以上です。最後までお読み頂きありがとうございます:)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした