SPA したくて React.js を始めたい人に手っ取り早く動かせるように tutorial を作りました。学習コストを下げるために redux を避けているので割ととっつきやすいと思います。たぶん。
この記事にあるソースコードはこちらに置いてあります。
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 との通信には axios
と parse-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 を使いたい場合は下記の記事を参考にして下さい。
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
に関しては下記の記事も参考にしてみてください。
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 }
以上です。最後までお読み頂きありがとうございます:)