アクションを送信して共有ロジックを呼び出すのはアンチパターン
公式ドキュメントで言及されていますが、共有ロジックを呼び出すためにアクションを送信するという実装パターンは非推奨とされています。
TCAにおいてアクションを送信するというのはパフォーマンス観点で高いコストを伴う処理であるためです。
以下は公式から引用したコードですが、.sharedComputation
というアクションに共有ロジックが実装されており、この共有ロジックを使用するために他のアクションから.sharedComputation
アクションが送信されています。
switch action {
case .buttonTapped:
state.count += 1
return .send(.sharedComputation)
case .toggleChanged:
state.isEnabled.toggle()
return .send(.sharedComputation)
case let .textFieldChanged(text):
state.description = text
return .send(.sharedComputation)
case .sharedComputation:
// Some shared work to compute something.
return .run { send in
// A shared effect to compute something
}
}
ReducerにEffectを返すメソッドを実装する
ではどうすればいいかというと、ReducerにEffectを返すメソッドを実装します。
これは公式ドキュメントで推奨されている実装パターンです。
sharedComputation
はReducerのメソッドなので、すべての依存関係にアクセスができます。
また、inout State
としてStateを受け取ることで、メソッド内でStateの更新も可能です。
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .buttonTapped:
state.count += 1
return self.sharedComputation(state: &state)
case .toggleChanged:
state.isEnabled.toggle()
return self.sharedComputation(state: &state)
case let .textFieldChanged(text):
state.description = text
return self.sharedComputation(state: &state)
}
}
func sharedComputation(state: inout State) -> Effect<Action> {
// Some shared work to compute something.
return .run { send in
// A shared effect to compute something
}
}
これは非常に有用な実装パターンですが、もう一つ、共有ロジックをより柔軟に扱える実装パターンをご紹介します。
ReducerにSendを引数に持つメソッドを実装する
Effectを返すメソッドは、Effect.runのクロージャ内で呼び出すことができません(呼び出すことができるにはできるんですが、※の箇所が実行されません)。
このため、例えば複数の共有ロジックを実行したいときに困ります。
Effect.mergeを使えばできるのではないか?というご意見をいただいたので、別のユースケースをこちらに書きました。合わせてご参照ください。
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .button1Tapped:
return sharedLogic1(state: state) // ここはOK
case .button2Tapped:
// どうやってsharedLogic1とsharedLogic2の2つのメソッドを実行する?
// 例えばこうすると
return .run { [state] send in
sharedLogic1(state: state) // ここで使おうとするとNG
sharedLogic2(state: state)
}
}
}
func sharedLogic1(state: State) -> Effect<Action> {
// ここは実行される
print("sharedLogic1")
return .run { send in
// ※ここが実行されない
await send(.fetchUserResponse(TaskResult {
try await userClient.fetch(state.userId)
}
}
}
func sharedLogic2(state: State) -> Effect<Action> { ... }
そこで、Sendを引数に持つasyncなメソッドに変更します。
こうすると、場所を選ばず柔軟に共有ロジックを呼び出すことができるようになります。
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .button1Tapped:
return .run { [state] send in
await sharedLogic1(send: send, state: state) // ここで使える
}
case .button2Tapped:
return .run { [state] send in
await sharedLogic1(send: send, state: state) // ここでも使える
await sharedLogic2(send: send, state: state)
}
}
}
func sharedLogic1(send: Send<Action>, state: State) async {
await send(.fetchUserResponse(TaskResult {
try await userClient.fetch(state.userId)
}
}
func sharedLogic2(send: Send<Action>, state: State) -> async { ... }
ただし、この実装パターンではメソッド内でStateを更新することはできません。
非同期コンテキスト内ではミュータブルな変数をキャプチャすることができないからです。
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .buttonTapped:
return .run { send in
// Mutable capture of 'inout' parameter 'state' is not allowed in concurrently-executing code
await sharedLogic(send: send, state: &state)
}
}
}
func sharedLogic(send: Send<Action>, inout state: State) async {
state.count += 1
await send(.fetchUserResponse(TaskResult {
try await userClient.fetch(state.userId)
}
}
なので、どのようにロジックを共通化するかによって、上で挙げた実装パターンを使い分けることになるかと思います。
なお、Sendを使った実装パターンはisowordsでも使用されています。