それはどうなるのでしょうか?
Redux(およびNGRX!)アプリケーションでより簡潔で表現力豊かなコードを書くのに役立つと思われる、いくつかの(具体的には5つの)メソッド、トリック、God of Enterpriseへの血まみれの犠牲について説明します。 道は汗とコーヒーに悩まされています。 強く蹴って批判してください。 一緒にコードを改善する方法を学びます。
正直なところ、最初は新しいマイクロライブラリ(35行のコード!) Flux-action-classについて世界中に伝えたかったのですが、Habrが間もなくTwitterになり、ほとんどの部分で感嘆符が増えているのを見て彼らに同意して、私はもう少し容量の多い読書をしようとすることにしました。 そこで、Reduxアプリケーションをアップグレードする5つの方法をご紹介します!
定型文が出てくる
ReduxにAJAXリクエストを送信する方法の典型的な例を考えてみましょう。 サーバーから実際にシールのリストが必要だと想像してみましょう。
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = (payload) => ({ type: actionTypeCatsGetSuccess, payload, }) const actionCatsGetError = (error) => ({ type: actionTypeCatsGetError, payload: error, }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
ここでセレクターのファクトリーが必要な理由がよくわからない場合は、こちらで読むことができます。
ここでは、意図的に副作用を考慮しません。 これは、10代の怒りと既存のエコシステムに対する批判に満ちた別の記事のトピックです。D
このコードにはいくつかの弱点があります。
- アクションファクトリーはそれ自体がユニークですが、アクションタイプを引き続き使用します。
- 新しいエンティティが追加されると、同じロジックを複製して
loading
フラグを設定し続けます。 dataに保存するdata
とその形式はリクエストごとに大きく異なる可能性がありますが、ダウンロードインジケーター(loading
フラグ)は同じです。 - スイッチの実行時間はO(n)です(まあ、 ほぼ )。 Reduxは原則としてパフォーマンスに関するものではないため、これ自体はあまり強力な議論ではありません。 それぞれの
case
2、3行余分にサービスコードを記述する必要があり、1つのswitch
を簡単に美しく分割して複数のswitch
分割することはできません。 - 各エンティティのエラー状態を個別に保存する必要が本当にありますか?
- セレクターはクールです。 メモ化されたセレクターは二重にクールです。 それらは私たちの側を抽象化するので、後でフォームを変更するときにアプリケーションの半分をやり直す必要がありません。 セレクター自体を変更するだけです。 目を楽しまないのは、 記憶の記憶の特殊性のためにのみ必要とされる原始的な工場のセットです。
方法1:アクションタイプを取り除く
まあ、そうでもない。 JSがそれらを作成するだけです。
一般にアクションタイプが必要な理由について少し考えてみましょう。 当然、レジューサーでロジックの目的のブランチを実行し、それに応じてアプリケーションの状態を変更します。 本当の質問は、型は文字列でなければならないということですか? しかし、クラスを使用してタイプごとにswitch
た場合はどうでしょうか?
class CatsGetInit {} class CatsGetSuccess { constructor(responseData) { this.payload = responseData } } class CatsGetError { constructor(error) { this.payload = error this.error = true } } const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.constructor) { case CatsGetInit: return { ...state, loading: true, } case CatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case CatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } }
すべてが素晴らしいように思えますが、1つの問題があります。アクションのシリアル化を失ったことです。 これらはもはや文字列に変換できる単純なオブジェクトではなく、その逆も可能です。 ここで、各アクションが独自のプロトタイプを持っているという事実に依存します。これにより、実際には、 action.constructor
switch
ような構造が機能します。 ご存知のように、アクションを文字列にシリアル化し、バグレポートと一緒に送信するというアイデアは本当に気に入っています。あきらめる準備はできていません。
そのため、各アクションにはtype
フィールドが必要です( ここでは、アクションを尊重するすべてのアクションに必要な他のものを確認できます)。 幸いなことに、すべてのクラスには文字列のような名前が付いています。 このクラスの名前を返すゲッターtype
各クラスに追加しましょう。
class CatsGetInit { constructor() { this.type = this.constructor.name } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.name: return { ...state, loading: true, } //... } }
それでも機能しますが、Eric氏がducks-modular-reduxで提案しているように、各タイプにプレフィックスを付けたいと思います(私にとっては、よりクールなre-ducksの分岐点を調べることをお勧めします)。 プレフィックスを追加するには、クラス名の直接使用を停止し、別のゲッターを追加する必要があります。 静的になりました。
class CatsGetInit { get static type () { return `prefix/${this.name}` } constructor () { this.type = this.constructor.type } } const reducerCats = (state, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } //... } }
この全体を少し見てみましょう。 コピーアンドペーストを最小限に抑えて、もう1つの条件を追加します。アクションがエラーの場合、 payload
のタイプはError
必要があります。
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { this.type = this.constructor.type this.payload = payload this.error = payload instanceof Error } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
この段階では、このコードはNGRXで正常に機能しますが、Reduxはそれを噛むことができません。 彼は、アクションは単純なオブジェクトであるべきだと誓います。 幸いなことに、JSを使用すると、コンストラクターからほとんどすべてを返すことができますが、アクションを作成した後、プロトタイプチェーンは実際には必要ありません。
class ActionStandard { get static type () { return `prefix/${this.name}` } constructor(payload) { return { type: this.constructor.type, payload, error: payload instanceof Error } } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case CatsGetInit.type: return { ...state, loading: true, } case CatsGetSuccess.type: return { error: undefined, data: action.payload, loading: false, } case CatsGetError.type: return { ...data, error: action.payload, loading: false, } default: return state } }
上記の考慮事項に基づいて、 flux-action-classマイクロライブラリが作成されました。 テストがあり、100%のテストカバレッジがあり、TypeScriptのニーズに合わせてジェネリックを使用したほぼ同じActionStandard
クラスがあります。 TypeScriptとJavaScriptの両方で動作します。
方法2:CombineReducersを使用することを恐れない
この考え方は簡単に恥をかかせます。combinedReducersは、トップレベルのレデューサーだけでなく、ロジックをさらに分解し、 loading
別のレデューサーを作成するためにもloading
ます。
const reducerLoading = (actionInit, actionSuccess, actionError) => ( state = false, action, ) => { switch (action.type) { case actionInit.type: return true case actionSuccess.type: return false case actionError.type: return false } } class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = (state = undefined, action) => { switch (action.type) { case CatsGetSuccess.type: return action.payload default: return state } } const reducerCatsError = (state = undefined, action) => { switch (action.type) { case CatsGetError.type: return action.payload default: return state } } const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, })
方法3:スイッチを取り除く
繰り返しますが、非常に単純なアイデアです。 switch-case
代わりに、キーを使用して目的のフィールドを選択するオブジェクトを使用します。 キーによるオブジェクトのフィールドへのアクセスはO(1)であり、私の謙虚な意見ではきれいに見えます。
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => { // const reducer = state[action.type] if (!reducer) { return state } // , return reducer(state, action) } const reducerLoading = (actionInit, actionSuccess, actionError) => createReducer(false, { [actionInit.type]: () => true, [actionSuccess.type]: () => false, [actionError.type]: () => false, }) class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCatsError = createReducer(undefined, { [CatsGetError.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerLoading(CatsGetInit, CatsGetSuccess, CatsGetError), error: reducerCatsError, })
reducerLoading
リファクタリングしましょう。 レデューサーのマップ(オブジェクト)がreducerLoading
、レデューサー全体を返す代わりに、 reducerLoading
からこのマップを返すことができます。 潜在的に、これは機能を拡張するための無限の範囲を開きます。
const createReducer = (initialState, reducerMap) => ( state = initialState, action, ) => { // const reducer = state[action.type] if (!reducer) { return state } // , return reducer(state, action) } const reducerLoadingMap = (actionInit, actionSuccess, actionError) => ({ [actionInit.type]: () => true, [actionSuccess.type]: () => false, [actionError.type]: () => false, }) class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) /* reducerCatsLoading: const reducerCatsLoading = createReducer( false, { ...reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ... some custom stuff } ) */ const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCatsError = createReducer(undefined, { [CatsGetError.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading), error: reducerCatsError, })
Reduxの公式ドキュメントもこのアプローチについて説明していますが 、何らかの未知の理由で、 switch-case
を使用する多くのプロジェクトを見続けています。 公式ドキュメントのコードに基づいて、Moshe氏はcreateReducer
用のライブラリをコンパイルしました。
方法4:グローバルエラーハンドラーを使用する
各エンティティのエラーを個別に保持する必要はありません。 ほとんどの場合、ダイアログを表示したいだけです。 すべてのエンティティのダイナミックテキストを含む同じダイアログボックス。
グローバルエラーハンドラを作成します。 最も単純な場合、次のようになります。
class GlobalErrorInit extends ActionStandard {} class GlobalErrorClear extends ActionStandard {} const reducerError = createReducer(undefined, { [GlobalErrorInit.type]: (state, action) => action.payload, [GlobalErrorClear.type]: (state, action) => undefined, })
次に、副作用として、 catch
でErrorInit
アクションを送信しcatch
。 redux-thunkを使用すると、次のようになります。
const catsGetAsync = async (dispatch) => { dispatch(new CatsGetInit()) try { const res = await fetch('https://cats.com/api/v1/cats') const body = await res.json() dispatch(new CatsGetSuccess(body)) } catch (error) { dispatch(new CatsGetError(error)) dispatch(new GlobalErrorInit(error)) } }
これで、catストアのerror
フィールドをCatsGetError
を使用してloading
フラグを切り替えることができます。
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) })
方法5:メモする前に考える
セレクターのファクトリーの山をもう一度見てみましょう。
前の章で見たように、 makeSelectorCatsError
は不要になったため、 makeSelectorCatsError
ました。
const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, )
ここにメモ化されたセレクターが必要なのはなぜですか? 私たちは正確に何をメモしようとしていますか? ここで行われるのは、キーによるオブジェクトフィールドへのアクセスで、O(1)です。 通常のメモされていない関数を使用できます。 メモ化は、ストアにあるデータを変更してからコンポーネントに渡す場合にのみ使用してください。
const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
メモ化は、結果をその場で計算する場合に意味があります。 以下の例では、各猫がname
フィールドを持つオブジェクトであり、すべての猫の名前を含む文字列を取得するとします。
const makeSelectorCatNames = () => createSelector( (state) => state.cats.data, (cats) => cats.data.reduce((accum, { name }) => `${accum} ${name}`, ''), )
おわりに
始めたところをもう一度見てみましょう。
import { createSelector } from 'reselect' const actionTypeCatsGetInit = 'CATS_GET_INIT' const actionTypeCatsGetSuccess = 'CATS_GET_SUCCESS' const actionTypeCatsGetError = 'CATS_GET_ERROR' const actionCatsGetInit = () => ({ type: actionTypeCatsGetInit }) const actionCatsGetSuccess = () => ({ type: actionTypeCatsGetSuccess }) const actionCatsGetError = () => ({ type: actionTypeCatsGetError }) const reducerCatsInitialState = { error: undefined, data: undefined, loading: false, } const reducerCats = (state = reducerCatsInitialState, action) => { switch (action.type) { case actionTypeCatsGetInit: return { ...state, loading: true, } case actionCatsGetSuccess: return { error: undefined, data: action.payload, loading: false, } case actionCatsGetError: return { ...data, error: action.payload, loading: false, } default: return state } } const makeSelectorCatsData = () => createSelector( (state) => state.cats.data, (cats) => cats, ) const makeSelectorCatsLoading = () => createSelector( (state) => state.cats.loading, (loading) => loading, ) const makeSelectorCatsError = () => createSelector( (state) => state.cats.error, (error) => error, )
そして、何に来た:
class CatsGetInit extends ActionStandard {} class CatsGetSuccess extends ActionStandard {} class CatsGetError extends ActionStandard {} const reducerCatsLoading = createReducer( false, reducerLoadingMap(CatsGetInit, CatsGetSuccess, CatsGetError), ) const reducerCatsData = createReducer(undefined, { [CatsGetSuccess.type]: () => action.payload, }) const reducerCats = combineReducers({ data: reducerCatsData, loading: reducerCatsLoading) }) const selectorCatsData = (state) => state.cats.data const selectorCatsLoading = (state) => state.cats.loading
あなたが時間を無駄にしないことを願っています。そして、この記事は少なくともあなたにとって有用でした。 冒頭で述べたように、一蹴して批判してください。 一緒にコードを改善する方法を学びます。