LoginSignup
58
39

More than 3 years have passed since last update.

カスタムフックとreact-reduxのHooks APIでビジネスロジックとビューを完全分離する

Last updated at Posted at 2019-09-01

ビジネスロジックをビューから分離する

React 16.8以降の機能でReact Hooksが使えます。
useStateやuseEffectなどを利用することでFunctinal Componentにstateやライフサイクルをもたせることが可能になります。
また、独自のカスタムフックを定義することでJSXからビジネスロジックの分離が可能になります。

カスタムフックの作成(公式)
https://ja.reactjs.org/docs/hooks-custom.html

Reactで普通に開発をしてしまうとコンポーネントが肥大化してしまう問題が発生するため、
カスタムフックを定義してビジネスロジックをビューから分離することでメンテナンスしやすくなります。(テストもしやすい)
カスタムフックはDOMを持たないので通常の関数と同じようにstateなどの値の戻り値を返却します。
ビューから分離できたため、ビジネスロジック部分をOSSとして提供することも可能です。
React Hooksのカスタムフックが実現する世界 - オブジェクト指向とOSS

さらに通常アプリケーションデータはReduxで管理することが多いと思いますが、
React-Redux 7.1以降でHooksに対応されたため、
https://react-redux.js.org/api/hooks
Reduxへのアクセスもカスタムフックで定義することが可能になりました。
このため、ビューとビジネスロジックの完全分離が可能です。(MVVMぽくなる)

サンプル作りました:https://github.com/teradonburi/learnReactJS/tree/ReactRedux-Hook

useActionsを定義する

ボイラーテンプレートとして、
次のようなuseActionsを定義してやります。
https://react-redux.js.org/api/hooks#recipe-useactions

useActions.js
import { bindActionCreators } from 'redux'
import { useDispatch } from 'react-redux'
import { useMemo } from 'react'

export default function useActions(actions, deps) {
  const dispatch = useDispatch()
  return useMemo(() => {
    if (Array.isArray(actions)) {
      return actions.map(a => bindActionCreators(a, dispatch))
    }
    return bindActionCreators(actions, dispatch)
  }, deps ? [dispatch, ...deps] : deps)
}

使い方

使い方は次のような感じです。
useUserHookというカスタムフックを作成します。
カスタムフックを作成する場合は、use〜という命名規則に従うようにします。
useSelectorがreact-reduxのconnectの第1引数(mapStateToProps)に相当します。(reduxのreducer経由から値を参照する)
actionsの呼び出しの方は、先程作成したuseActionsを使います。
Appコンポーネント側はデータに関する取得のみ、useUserHookより受け取りレンダリングのみ行っています。(ビジネスロジックを分離しました)
今回はuseSelectorとuseActionsをuseUserHookにまとめてしまっていますが、
実用途では別のフックに分けて読み込みのuseActionsの方はページ単位に1回呼ぶなどにしたほうが良いと思います。

App.jsx
import React, { useEffect } from 'react'
import { useSelector } from 'react-redux'
import useActions from './useActions'
import { load } from './user'

function useUserHook() {
  const users = useSelector(state => state.user.users)
  const [loadUser] = useActions([load])
  console.log(users)

  useEffect(() => {
    loadUser()
  }, [])
  return users
}

function App() {
  const users = useUserHook()

  return (
    <div>
      {users.map((user) => (
        // ループで展開する要素には一意なkeyをつける(ReactJSの決まり事)
        <div key={user.email}>
          <img src={user.picture.thumbnail} />
          <p>名前:{user.name.first + ' ' + user.name.last}</p>
          <p>性別:{user.gender}</p>
          <p>email:{user.email}</p>
        </div>
      ))}
    </div>
  )
}

export default App

ちなみにuser.jsのreducerやactionsの定義は次のようにしています。
redux-thunksとaxiosで非同期のAPI呼び出しをしています。
今回はサンプルなのでRandom UserというサービスのAPIでユーザ取得を行っています。

user.js
// reducerで受け取るaction名を定義
const LOAD = 'user/LOAD'

// 初期化オブジェクト
const initialState = {
  users: [],
}

// reducerの定義(dispatch時にコールバックされる)
export default function reducer(state = initialState, action = {}){
  // actionの種別に応じてstateを更新する
  switch (action.type) {
    case LOAD:
      return {
        users:action.results,
      }
    default:
      // 初期化時はここに来る(initialStateのオブジェクトが返却される)
      return state
  }
}

// actionの定義
export function load() {
  // clientはaxiosの付与したクライアントパラメータ
  // 非同期処理をPromise形式で記述できる
  return (dispatch, getState, client) => {
    return client
      .get('https://randomuser.me/api/')
      .then(res => res.data)
      .then(data => {
        const results = data.results
        // dispatchしてreducer呼び出し
        dispatch({ type: LOAD, results })
      })
  }
}

初期化処理です。
reduxのstore作成とredux-thunkミドルウェアを指定してます。(connect使うときと同じ)

index.jsx
import React  from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
import { Provider } from 'react-redux'
import client from 'axios'
import thunk from 'redux-thunk'

import App from './App.jsx'
import user from './user'

const reducer = combineReducers({
  user,
})

// redux-devtoolの設定
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
// axiosをthunkの追加引数に加える
const thunkWithClient = thunk.withExtraArgument(client)
// redux-thunkをミドルウェアに適用
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunkWithClient)))

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)
58
39
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
58
39