ビジネスロジックをビューから分離する
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]
(https://qiita.com/sonatard/items/617f324228f75b9c802f)
さらに通常アプリケーションデータは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
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回呼ぶなどにしたほうが良いと思います。
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でユーザ取得を行っています。
// 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使うときと同じ)
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')
)