19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TCA】Reducerの共有ロジックの実装パターン

Last updated at Posted at 2023-07-27

アクションを送信して共有ロジックを呼び出すのはアンチパターン

公式ドキュメントで言及されていますが、共有ロジックを呼び出すためにアクションを送信するという実装パターンは非推奨とされています。
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でも使用されています。

19
3
2

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
19
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?