あいつの日誌β

働きながら旅しています。

React tutorial (4)

あらすじ

前回は redux による state の管理について触れました。 今回は http 通信などによる非同期処理の方法について触れます。

なお外部との通信に https://jsonplaceholder.typicode.com を利用しています。

目次

  • 開発環境を準備
  • React の基本的な Life Cycle に触れる
  • redux に触れる
  • redux-saga に触れる <= 今日やること
  • react router に触れる
  • npm で公開されている components を導入して echo system を体感する
  • redux-form に触れる
  • react-select に触れる

準備

npm install --save redux-saga axios
npm install --save-dev http-proxy-middleware
touch src/api.js src/sagas.js

create src/api.js

import axios from 'axios'

const BASE_URL = window.location.origin

export function getPosts({userId}) {
  return axios.get(`${BASE_URL}/api/posts?userId=${userId}`).then((res) => {
    return { "posts": res.data }
  })  
}

export default { getPosts }

edit src/actions.js

import { createAction } from "redux-actions"
import api from './api'

export const FETCH_REQUESTED = "FETCH_REQUESTED"
export const FETCH_SUCCESSED = "FETCH_SUCCESSED"
export const FETCH_FAILED    = "FETCH_FAILED"

export const fetchRequested = createAction(FETCH_REQUESTED)
export const fetchSuccessed = createAction(FETCH_SUCCESSED)
export const fetchFailed    = createAction(FETCH_FAILED)

export function getPosts({userId}) {
  return fetchRequested(() => {
    return api.getPosts({userId})
  })
}

edit src/reducers/index.js

import { combineReducers } from 'redux'
import { FETCH_SUCCESSED } from '../actions'

const initialState = { 
  posts: []
}

function fetch(state = initialState, action) {
  switch (action.type) {
  case FETCH_SUCCESSED:
    return Object.assign({}, state, action.payload)
  default:
    return state
  }
}

const rootReducer = combineReducers({
  fetch,
})

export default rootReducer

edit src/store/configureStore.js

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import createLogger from 'redux-logger'

import rootReducers from '../reducers'
import rootSaga from "../sagas"

const sagaMiddleware = createSagaMiddleware()
const logger = createLogger()

const middlewares = [sagaMiddleware, logger]
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)

export default function configureStore(initialState) {
  const store = createStoreWithMiddleware(rootReducers, initialState)
  if (module.hot) {
    module.hot.accept('../reducers', () => {
      const nextRootReducer = require('../reducers').default
      store.replaceReducer(nextRootReducer)
    })
  }
  sagaMiddleware.run(rootSaga)
  return store
}

create src/sagas

import { takeEvery } from "redux-saga"
import { put, call } from "redux-saga/effects"
import api from "./api"

import {
  FETCH_REQUESTED,
  fetchSuccessed,
  fetchFailed,
} from "./actions"

function* fetch(action) {
  try {
    const data = yield call(action.payload)
    yield put(fetchSuccessed(data))
  } catch (err) {
    yield put(fetchFailed(err))
  }
}

export default function* rootSaga() {
  yield takeEvery(FETCH_REQUESTED, fetch)
}

edit src/containers/Posts.js

import React, {Component} from 'react'
import { connect } from 'react-redux'
import { fetchRequest } from '../actions'
import { PostItem } from '../components/Posts'
import { getPosts } from '../actions'

class Posts extends Component {

  handleButtonClick() {
    this.props.dispatch(getPosts({userId: 1}))
  }

  render() {
    const {posts} = this.props.fetch

    return (
      <div className="container">
        <h2>Posts</h2>
        {posts.map((post) => (
          <PostItem key={post.id} post={post} />
        ))}
        <button className="btn btn-default" onClick={this.handleButtonClick.bind(this)}>Push</button>
      </div>
    )
  }
}

const select = state => (state)

export default connect(select)(Posts)

edit devServer.js

const webpack = require('webpack')
const browserSync = require('browser-sync')
const proxy = require('http-proxy-middleware')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const historyApiFallback = require('connect-history-api-fallback')

const config = require('./webpack.config')
const bundler = webpack(config)

const apiProxy = proxy('/api', {
  pathRewrite: {'^/api' : ''},
  target: 'https://jsonplaceholder.typicode.com',
  changeOrigin: true,
  logLevel: 'debug'
})

browserSync({
  open: false,
  server: {
    baseDir: config.output.path,
    index: "index.html",
    middleware: [
      apiProxy,
      webpackDevMiddleware(bundler, {
        publicPath: config.output.publicPath,
        stats: { colors: true }
      }),
      webpackHotMiddleware(bundler),
      historyApiFallback()
    ]
  },
  files: [
    'www/*.html'
  ]
})

Implement it and click button.

% node devServer.js

説明

非同期での処理の場合はイベントの発生を FETCH_REQUESTED, FETCH_SUCCESSED, FETCH_FAILED とします。 こうすることで WEB API への通信が開始してから完了するまで loading bar を表示したり、通信の成否に応じて Notification を表示することができます。

まとめ

今回は redux による非同期処理について触れました。 次回は react router について触れます。

React tutorial (3)

あらすじ

前回は React の基本的な作法に触れました。 今回は Redux を使ってみます。

目次

  • 開発環境を準備
  • React の基本的な Life Cycle に触れる
  • redux に触れる <= 今日やること
  • redux-saga に触れる
  • react router に触れる
  • npm で公開されている components を導入して echo system を体感する
  • redux-form に触れる
  • react-select に触れる

今日やること

f:id:okamuuu:20170122153154g:plain

準備

npm install --save redux redux-actions redux-logger react-redux
mkdir src/containers src/store src/reducers
touch src/containers/Posts.js src/actions.js src/store/configureStore.js src/reducers/index.js

各種ファイル作成

create src/actions.js

import { createAction } from "redux-actions"

export const SET_POSTS = 'SET_POSTS'

export const setPosts = createAction(SET_POSTS)

create src/reducers/index.js

import { combineReducers } from 'redux'
import { SET_POSTS } from '../actions'

const initialState = { 
  posts: []
}

function app(state = initialState, action) {
  switch (action.type) {
  case SET_POSTS:
    return Object.assign({}, state, {posts: action.payload})
  default:
    return state
  }
}

const rootReducer = combineReducers({
  app,
})

export default rootReducer

create src/store/configureStore.js

import { createStore, applyMiddleware } from 'redux'
import createLogger from 'redux-logger'
import rootReducers from '../reducers'

const logger = createLogger()
const middlewares = [logger]
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)

export default function configureStore(initialState) {
  const store = createStoreWithMiddleware(rootReducers, initialState)
  if (module.hot) {
    module.hot.accept('../', () => {
      const nextRootReducer = require('../reducers').default
      store.replaceReducer(nextRootReducer)
    })  
  }
  return store
}

create src/containers/Posts.js

import React, {Component} from 'react'
import { connect } from 'react-redux'
import { setPosts } from '../actions'
import { PostItem } from '../components/Posts'
import { posts } from '../fixtures'

class Posts extends Component {

  handleButtonClick() {
    this.props.dispatch(setPosts(posts))
  }

  render() {
    const {posts} = this.props.app

    return (
      <div className="container">
        <h2>Posts</h2>
        {posts.map((post) => (
          <PostItem key={post.id} post={post} />
        ))}
        <button className="btn btn-default" onClick={this.handleButtonClick.bind(this)}>Push</button>
      </div>
    )
  }
}

const select = state => (state)

export default connect(select)(Posts)

edit src/Root.js

import 'babel-polyfill'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.css'
import React from 'react'
import { Provider } from 'react-redux'

import configureStore from './store/configureStore'
import Posts from './containers/Posts'

const store = configureStore()

export default (props) => (
  <Provider store={store}>
    <Posts />
  </Provider>
)

Implement it

% node devServer.js

edit src/components/Posts.js to remove wasted codes

import React, {Component} from 'react'

// stateless component
export const PostItem = (props) => (
  <div>
    <h3>{props.post.title}</h3>
    <p>{props.post.body}</p>
  </div>
)

containers と conmponents について

src/containerssrc/components の違いについて明確な答えを私はよく知らないのですが、とりあえず connect を使っているコンポーネントは containers に配置する。でいいと思います。 で、connect していないものは containers に入れてはいけないのか?というとそうではないので、とりあずよく分からない場合は全部 containers に入れておいていいと思います。

私は共通化できる stateless な コンポーネントを見つけたときだけ src/components に移動させるようにしています。

それから最初はconnect が何やっているかわかりづらいと思います。これは親、子、孫 のコンポーネントがあった場合、親から孫までデータ渡して孫から親までイベントを伝播させるのではなく、直接孫にデータを渡して、イベントを引き取るために使っている。というイメージで覚えておいてとりあえず次に進んでしまってください。

まとめ

今回は redux について触れました 次回は redux-thunk をすっとばして redux-saga について触れます。

React tutorial(2)

あらすじ

前回は開発環境の構築方法を紹介しました。 今回はReact で使われる簡単な記述方法に触れます。

たぶん直感で理解できると思いますので細かい説明は省略します。

目次

  • 開発環境を準備
  • React の基本的な Life Cycle に触れる <= 今日やること
  • redux に触れる
  • redux-saga に触れる
  • react router に触れる
  • npm で公開されている components を導入して echo system を体感する
  • redux-form に触れる
  • react-select に触れる

今日やること

こんな感じの画面を作ります

f:id:okamuuu:20170122150249p:plain

各種ファイル作成

mkdir src/components
touch src/fixtures.js src/components/Posts.js

create src/fixtures.js

export const posts = [{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
}]

create src/components/Posts.js

import React, {Component} from 'react'

// stateless component
export const PostItem = (props) => (
  <div key={props.post.id}>
    <h3>{props.post.title}</h3>
    <p>{props.post.body}</p>
  </div>
)

export default class Posts extends Component {

  constructor(props) {
    console.log("=== constructor ===")
    super(props)
  }

  componentWillMount() {
    console.log("=== componentWillMount ===")
  }

  componentDidMount() {
    console.log("=== componentDidMount ===")
  }

  componentWillReceiveProps() {
    console.log("=== componentWillReceiveProps ===")
  }

  componentWillUpdate() {
    console.log("=== componentWillUpdate ===")
  }

  componentDidUpdate() {
    console.log("=== componentDidUpdate ===")
  }

  componentWillUnmount() {
    console.log("=== componentWillUnmount ===")
  }

  handleButtonClick() {
    alert("Button Clicked!!")
  }

  render() {
    const {posts} = this.props

    return (
      <div className="container">
        <h2>Posts</h2>
        {posts.map((post) => (
          <PostItem post={post} />
        ))}
        <button className="btn btn-default" onClick={this.handleButtonClick.bind(this)}>Push</button>
      </div>
    )
  }
}

edit src/Root.js

import 'babel-polyfill'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.css'
import React from 'react'

import {posts} from './fixtures'
import Posts from './components/Posts'

export default (props) => (
  <Posts posts={posts} />
)

Implement it

% node devServer.js

まとめ

今回は React の LifeCycle と、よく使われる関数の書き方である handleButtonClickposts.map に触れました。 LifeCycle に関しては都度ドキュメントを見て細かい事を覚えながらでも大丈夫だと思います。

とりあえず render() 関数に必要なデータを渡せば画面をレンダリングできる、ということがなんとなく理解できたと思います。 次回は redux をつかってどうやって render 関数にデータを渡すのかについて触れたいと思います。

React Tutorial (1)

追記(2017-02-08)

webpack.config.js がエラーが出るので訂正しました。js 界隈は変化が早いのでチュートリアルを試される方はお早めにお願いします。 あと React.js の tutorial というよりも redux の tutorial ではないかという意見を頂戴しましたがよくよく考えればそうですね。

あらすじ

React, redux を使った開発をしたのですが、後続の部隊がきちんとメンテできるかどうか心配だったのでドキュメントを作成しました。

目次

  • 開発環境を準備 <= ココ
  • React の基本的な Life Cycle に触れる
  • redux に触れる
  • redux-saga に触れる
  • react router に触れる
  • npm で公開されている components を導入して echo system を体感する
  • redux-form に触れる
  • react-select に触れる

開発環境を準備

browsersync + webpack の設定を行います。 react hot replace の設定をしておく作業が早くなる気がするので組み込みます。

今回の作業で必要なファイル、ディレクトリを作成します。

mkdir practice-react && cd $_
mkdir src www
touch .babelrc webpack.config.js devServer.js src/index.js src/Root.js www/index.html
npm init -y

開発でのみ使用する module は devDependencies にインストールします。

npm install --save-dev browser-sync webpack webpack-dev-middleware webpack-hot-middleware
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react babel-plugin-react-hot
npm install --save-dev style-loader file-loader css-loader url-loader react-hot-loader

bundle.js に組み込まれる module は dependencies にインストールします。

npm install --save babel-polyfill
npm install --save jquery bootstrap
npm install --save react react-dom

各種ファイル作成

create .babelrc

{
  "presets": ["es2015", "stage-0", "react"]
}

create webpack.config.js

const path = require('path')
const webpack = require('webpack')

module.exports = {
  devtool: 'inline-source-map',
  entry: [
    'webpack/hot/dev-server',
    'webpack-hot-middleware/client',
    './src/index.js'
  ],
  output: {
    publicPath: "/",
    path: path.resolve('www'),
    filename: "bundle.js",
  },
  plugins: [
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.ProvidePlugin({
      jQuery: 'jquery',
      $: "jquery"
    })
  ],
  resolve: {
    extensions: ['.js'],
  },
  module: {
    loaders:[
      { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel-loader'] },
      { test: /\.css$/, loader: "style-loader!css-loader" },
      { test: /\.svg$/, loader: 'url-loader?mimetype=image/svg+xml' },
      { test: /\.woff$/, loader: 'url-loader?mimetype=application/font-woff' },
      { test: /\.woff2$/, loader: 'url-loader?mimetype=application/font-woff' },
      { test: /\.eot$/, loader: 'url-loader?mimetype=application/font-woff' },
      { test: /\.ttf$/, loader: 'url-loader?mimetype=application/font-woff' }
    ]
  }
}

create devServer.js

const webpack = require('webpack')
const browserSync = require('browser-sync')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const historyApiFallback = require('connect-history-api-fallback')

const config = require('./webpack.config')
const bundler = webpack(config)

browserSync({
  open: false,
  server: {
    baseDir: config.output.path,
    index: "index.html",
    middleware: [
      webpackDevMiddleware(bundler, {
        publicPath: config.output.publicPath,
        stats: { colors: true }
      }),
      webpackHotMiddleware(bundler),
      historyApiFallback()
    ]
  },
  files: [
    'www/*.html'
  ]
})

create src/index.js

import React from 'react'
import { render } from 'react-dom'
import Root from './Root'

render(
  <Root />, 
  document.getElementById('root')
)

if (module.hot) {
  module.hot.accept('./Root', function() {
    const NextApp = require('./Root').default
    render(<NextApp/>, document.getElementById('root'))
  })  
}

create src/Root.js

import 'babel-polyfill'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.css'
import React from 'react'

export default (props) => (
  <div className="container">
    <h1>React boilerplate</h1>
    <p>test</p>
  </div>
)

create www/index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://opensource.keycdn.com/fontawesome/4.7.0/font-awesome.min.css" integrity="sha384-dNpIIXE8U05kAbPhy3G1cz+yZmTzA6CY8Vg/u2L9xRnHjJiAK76m2BIEaSEV+/aU" crossorigin="anonymous">
  </head>
  <body>
    <div id="root"></div>
    <script src="/bundle.js"></script>
  </body>
</html>

以下を実行

% node devServer.js

まとめ

今回は開発環境の構築方法についての説明を行いました。 Browsersync で通信速度を 3G に設定して動作確認するとシャレオツな感じで良いと思います。 次回は基本的な Life cycle と記述方法について説明します。

Javascript で複数ある正規表現のいずれかがマッチしているものを取得する

以下のように Javascript の switch 文を使おうかなと思いました。

http://qiita.com/piyohiko/items/a84648599eba7697675f

なんですが、判定したいだけじゃなく、マッチした要素を抽出したいのでなんとなく OR 演算子を使いました。

const githubRegex = new RegExp("https://github.com/([^/]*)")
const bitBucketRegex = new RegExp("https://bitbucket.org/([^/]*)")
const url1 = "https://github.com/github-user"
const url2 = "https://bitbucket.org/bitbucket-user/"

function extractUserName(url) {
  const result = githubRegex.exec(url) || bitBucketRegex.exec(url)
  if (!result) {
    return
  }
  return result[1]
}

console.log(extractUserName(url1)) // github-user
console.log(extractUserName(url2)) // bitbucket-user

おしまい。

MacOSX で複数ファイル一括置換

あらすじ

familyName を間違えて famillyName としてしまった時のお話です

sed コマンドオプションでちょっと迷ったので備忘録

とりあえず対象となるファイルを検索

この時対象となっているファイルは js ファイルだったので以下のようにして対象となるファイル一覧を取得します。 これは別に find を使っても良いと思います。

% grep -l 'famillyName' **/*.js  

パイプする方法を確認する

xargs を使うと良いでしょう。

% grep -l 'famillyName' **/*.js  | xargs echo

sed にパイプする

echo ではなく sed にパイプします。バックアップ要らない場合、MacOXS だと以下のように空文字をいれると良いらしい。

% grep -l 'famillyName' **/*.js  | xargs sed -i "" -e 's/famillyName/familyName/g'

おしまい

App Enigine に deploy しよとすると composite literal uses unkeyed fields と言われた件

あらすじ

ある日手元で開発したソースコードを App Engine にデプロイしようとするとエラーが出たときのお話です。

問題と解決策

問題となったのは以下のコード

func NewNullString(s string) NullString {
    return NullString{sql.NullString{s, s != ""}}
}

ここは以下のように書き直すと deploy できました。

func NewNullString(s string) NullString {
    return NullString{sql.NullString{
        String: s,
        Valid:  s != "", 
    }}
}

解説

手元ではコンパイルできるんだけど App Engine にデプロイできない理由は App Engine 側でコンパイル前にエラーチェックをしていてそれに引っかかっている事が原因のようです。

表示されたエラーの意味は composite struct literal database/sql.NullString with unkeyed fields で struct を複数合わせ技を使っているので 「unkeyed fields 使っているけど大丈夫なのかい?んん?」と言っているようです。

で、そのエラーの発言主は go-vet です。このコマンドはコンパイラで見つけられないエラーを検出することができます。 実際にこのコマンドを手元の MacOSX で実行すると同じエラーが表示されます。

% go tool vet model/model.go
model/model.go:31: database/sql.NullString composite literal uses unkeyed fields

別の解法としてはそのチェックをしないようにしても良いです。ただ私は前述の解決策で良いので App Engine 側でチェックの可否をコントロールする方法はよくわかってないです。

% go tool vet --composites=false model/model.go # 怒られない
% go tool vet --composites=true  model/model.go # 怒られる
model/model.go:31: database/sql.NullString composite literal uses unkeyed fields

こちらからは以上です。