Send an action from one reducer to another

Hello, I want to achieve an effect similar to this:

So, I have a workout state, which holds var sets: IdentifiedArrayOf<ActiveExerciseRowState> = [] and shows the exercises as follows:

      ForEachStore(
        self.store.scope(state: \.sets, action: ActiveWorkoutAction.exerciseSet(id:action:)),
        content: ActiveExerciseRowView.init(store:)
      )

with the following reducer:

public let activeWorkoutReducer = Reducer<ActiveWorkoutState, ActiveWorkoutAction, ActiveWorkoutEnvironment>.combine(
Reducer { state, action, environment in
  switch action {
  case .exerciseSet(let id, let rowAction):
    switch rowAction {
    case .exerciseFinished:
      state.moveToNextExercise()
    default: break
    }
  }
  return .none
},
activeExerciseRowReducer.forEach(
  state: \.sets,
  action: /ActiveWorkoutAction.exerciseSet(id:action:),
  environment: { _ in ActiveExerciseRowEnvironment(mainQueue: DispatchQueue.main.eraseToAnyScheduler()) } )
)

The exercise has an action exerciseBegin with the following implementation:

case .exerciseBegin:
  state.secondsLeft = state.set.duration
  return Effect
    .timer(id: TimerId(), every: 1, tolerance: .zero, on: environment.mainQueue)
    .map { _ in ActiveExerciseRowAction.timerTicked }

Since the workout knows which exercise should be next, how can I send the exerciseBegin from the workout reducer to the corresponding exercise reducer?

Hey @smeshko!

@mbrandonw actually sketched out a resending higher-order reducer in this thread: Responding to `FooClient.Action` regardless of which view generates `Effect`s emitting them - #2 by mbrandonw

You could maybe tack that onto activeWorkoutReducer:

let activeWorkoutReducer = Reducer.combine(
  ...
)
.resending(..., to: ...)

Hello @stephencelis and thanks for the quick response! Could you maybe elaborate a bit more on the solution? I am not sure how to use the resending with a variadic number of elements and having 2 different types of Actions in the case and to. Thanks!

Can you post a lil more code so I can see the action definition, etc.? The from and to don't have to match exactly, but you will need to have all the information you need in the from case to construct a to case.

An abstract example of forwarding a String action to a Bool action:

enum ChildActionA { case foo(String) }
enum ChildActionB { case bar(Bool) }
enum ParentAction { case childA(ChildActionA), childB(ChildActionB) }

...

let reducer = Reducer<Void, ParentAction, Void>.empty
  // types very explicit:
  .resending(
    from: { (string: String) -> ParentAction in
      ParentAction.childA(ChildActionA.foo(string))
    },
    to: { (string: String) -> ParentAction in
      ParentAction.childB(ChildActionB.bar(string.count > 5))
    }
  )
  // or, with abbreviated types:
  .resending(
    from: { .childA(.foo($0)) }, to: { .childB(.bar($0.count > 5)) }
  )

is "resending" deprecated?

@mbrandonw defined resending here: Responding to `FooClient.Action` regardless of which view generates `Effect`s emitting them - #2 by mbrandonw

I don't think it was ever part of a package release.

It's definitely not deprecated, it just doesn't come with TCA. It might be useful enough to include some day, but it's simple enough that we recommend folks copy and paste it into their projects to use it :smile:

As Brandon's resending implementation uses the now deprecated CasePath.case(case), I thought I'd give it a shot and reimplement it using CasePaths.

extension Reducer {
  func resending<Value>(
    _ extract: @escaping (Action) -> Value?,
    to embed: @escaping (Value) -> Action
  ) -> Self {
    .combine(
      self,
      .init { _, action, _ in
        if let value = extract(action) {
          return Effect(value: embed(value))
        } else {
          return .none
        }
      }
    )
  }

  func resending<Value>(
    _ `case`: CasePath<Action, Value>,
    to other: CasePath<Action, Value>
  ) -> Self {
    resending(`case`.extract(from:), to: other.embed(_:))
  }

  func resending<Value>(
    _ `case`: CasePath<Action, Value>,
    to other: @escaping (Value) -> Action
  ) -> Self {
    resending(`case`.extract(from:), to: other)
  }

  func resending<Value>(
    _ extract: @escaping (Action) -> Value?,
    to other: CasePath<Action, Value>
  ) -> Self {
    resending(extract, to: other.embed(_:))
  }
}

Usage

parentReducer
  .resending(/ParentAction.fooClient, to: /ParentAction.child1 .. Child1Action.fooClient)
  .resending(/ParentAction.fooClient, to: /ParentAction.child2 .. Child2Action.fooClient)
  .resending(/ParentAction.fooClient, to: { value in .boolAction(value > 5) })

If extensions like this one don't have 'a place' in the core lib, should we (as a community) maybe create a space similar to CombineExt and RxSwiftExt that is managed by a TCACommunity?

1 Like

My bad :person_facepalming:t2:, read the solution assuming it was part of the framework

Please consider opening a PR on GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind..

This is a more convient way to do it(today I have reducers just for that).

BTW is there a more compact way to write this ?

  .resending(
    { action in
      if case let .local(.fetchFromCloud(recordKey)) = action { return recordKey }
      return nil
    }, to: { .core(.fetchFromCloud($0)) }
  )

Thanks,