redux/examples/real-world を見て学ぶ

GitHub - reactjs/redux: Predictable state container for JavaScript apps

Githubのアカウント名を入力すると、 starしているリポジトリと作者の一覧が表示される。

一覧表示のリポジトリ名やアカウント名をクリックすると、それぞれ対応したページに切り替わる

ページ遷移

GitHub - acdlite/redux-router: Redux bindings for React Router – keep your router state inside your Redux store

redux-router を活用してアカウント、リポジトリをURLで表現することで、ページの遷移を実現している。

以下のコードで、アカウント名を入力してgoボタンを押すと、"/アカウント名" に遷移する処理を実現。

// container/App.jsから対象部分を抜粋

import { browserHistory } from 'react-router'
import Explore from '../components/Explore'
...

class App extends Component {
  ...
  handleChange(nextValue) {
    browserHistory.push(`/${nextValue}`)
  },
  ...
  render() {
    const { children, inputValue } = this.props
    return (
      <div>
        <Explore value={inputValue}
                 onChange={this.handleChange} />
        <hr />
        {this.renderErrorMessage()}
        {children}
      </div>
    )
  }
  ...
}
// container/Explore.js内

export default class Explore extends Component {
  ...
  handleGoClick() {
    this.props.onChange(this.getInputValue())
  },
  ...
}

ページ遷移後のデータ取得

処理の流れは下記のようなイメージ。

  • URLからアカウント名 or リポジトリ名を取得して、github apiをcallするアクションを実行
  • api callを処理するmiddlewareでアクションを type:REQUEST, type:SUCCESS or FAILURE に分割したアクションを生成
  • SUCCESSを処理するreducerからstateを生成

URLからアカウント名 or リポジトリ名を取得して、github apiをcallするアクションを実行

redux-routerの機能で、URLに応じて、読み込むコンポーネントを切り分けて、さらにURLからコンポーネントへ渡すPropsを定義している。

// ./routes.js
export default (
  <Route path="/" component={App}>
    <Route path="/:login/:name"
           component={RepoPage} />
    <Route path="/:login"
           component={UserPage} />
  </Route>
)

この記述により、例えばUserPageの場合、mapStateToProps内で、 アカウント名を取得することが可能

// containers/UserPage.js

...

function mapStateToProps(state, ownProps) {
  ... 
  const login = ownProps.params.login.toLowerCase() // アカウント名を取得
  ...
}

UserPage#componentWillMount でアカウント名、Github APIからデータを取得するアクションをdispatchする

// containers/UserPage.js

...
import { loadUser, loadStarred } from '../actions'
...

function loadData(props) {
  const { login } = props
  props.loadUser(login, [ 'name' ])
  props.loadStarred(login)
}

...

class UserPage extends Component {
  ...
  componentWillMount() {
    loadData(this.props)
  }
  ...
}

...

export default connect(mapStateToProps, {
  loadUser,
  loadStarred
})(UserPage)

CALL API アクションのdispatch

上記のUserPageコンポーネントでimportされる loadUser の定義.

// actions/index.js

...

function fetchUser(login) {
  return {
    [CALL_API]: {
      types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ],
      endpoint: `users/${login}`,
      schema: Schemas.USER
    }
  }
}

// Fetches a single user from Github API unless it is cached.
// Relies on Redux Thunk middleware.
export function loadUser(login, requiredFields = []) {
  return (dispatch, getState) => {
    const user = getState().entities.users[login]
    if (user && requiredFields.every(key => user.hasOwnProperty(key))) {
      return null
    }

    return dispatch(fetchUser(login))
  }
}

...

fetchUserで定義されているフォーマットは、

GitHub - agraboso/redux-api-middleware: Redux middleware for calling an API.

で定義されているAPI CALLを行うアクションのフォーマット。

アクションを処理するmiddleware

storeにmiddlewareを設定することで、特定のフォーマットのアクションへの処理を追加することができる。

例えば

GitHub - agraboso/redux-api-middleware: Redux middleware for calling an API.

を導入すると、上記のAPI CALL フォーマットのアクションを適切に処理してくれるようになる。

今回のexampleでは.middleware/api.js に自前で実装されている。

やっていることは

  • actionが対象のフォーマットかどうかを確認. 対象でなければ何もしないで終了
  • REQUEST typeのアクションを実行
  • api call後にSUCCESS or FAILURE typeのアクションをレスポンスをくっつけて実行

感想

stateの持ち方はかなりシンプルにして、middlewareやcomponentのcomponentWillMount, componentWillReceivePropsを活用することで、複雑な実装をクリアにしている感じが参考になった。