TCA で Reducer を combine する順序は意識しなければいけない
TCA は画面や Component ごとに Reducer を分割していくことができ、分割した Reducer を結合することによって tree 状の State を容易に構築することができます。
モジュールとしての独立性を高めるために、これが容易にできることはありがたいことですが、「分割した Reducer を結合する」際に気をつけなければいけないことがあります。
それは Reducer を combine する順序です。
Reducer は以下のように combine することができます。(この記事では combine や pullback についての詳しい説明はしません)
let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
childReducer.optional().pullback(
state: \.child,
action: /ParentAction.child,
environment: { $0.child }
),
Reducer { state, action, environment in
switch action
case .child(.dismiss):
state.child = nil
return .none
...
}
},
)
これは正しい例ですが、以下のように combine することは実は誤っています。
let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
Reducer { state, action, environment in
switch action
case .child(.dismiss):
state.child = nil
return .none
...
}
},
childReducer.optional().pullback(
state: \.child,
action: /ParentAction.child,
environment: { $0.child }
)
)
最初の例と上記で異なるのは Reducer を combine している順序です。
それぞれ以下のように combine しています。
- 最初の例
-
childReducer
を先に書いてから、Reducer
を combine している
-
- 2 番目の例
-
Reducer
を先に書いてから、childReducer
を cobmine している
-
2 番目の例は正しいですが、最初の例のように Reducer を combine してしまうと意図しないバグを起こす可能性があります。
実はその話はしっかりと TCA の Reducer.swift
の combine などの Documentation comment に記載されています。
/// It is important to note that the order of combining reducers matter. Combining `reducerA` with
/// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`.
「Reducer の組み合わせの順序は重要。reducerA
と reducerB
を組み合わせることは、reducerB
と reducerA
を組み合わせることと同じとは限らない」ということが書かれています。
combine の順序を誤るとどういった場合に意図しないバグを引き起こすかについても、同じく comment に記載されています。
例えば以下のような例を考えてみます。
let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
childReducer.optional().pullback(
state: \.child,
action: /ParentAction.child,
environment: { $0.child }
),
Reducer { state, action, environment in
switch action
case .child(.dismiss):
state.child = nil
return .none
...
}
},
)
このように combine した場合、基本的に childReducer
の処理が先に実行され、Reducer
の処理は後に実行されます。
では、combine する順序を逆にするとどうなるでしょうか。
let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
Reducer { state, action, environment in
switch action
case .child(.dismiss):
state.child = nil
return .none
...
}
},
childReducer.optional().pullback(
state: \.child,
action: /ParentAction.child,
environment: { $0.child }
)
)
こうしてしまうと Reducer
で child state を nil にしてしまった後で childReducer
の処理が走るという構造になってしまい、child state が nil になっているため childReducer
は自分の action に反応することができなくなってしまいます。
そのため、基本的に Reducer の combine は「child reducer を先に結合しなければならない」と考えておくと良いと思います。
Discussion