How to Scope with Reducer Protocol and convert Scope to ScopeOf?

I assume this is how "pullback" is done in the new Reducer Protocol, please correct me if I'm wrong.

This is the ProductDomain (Parent) and its SwiftUI View which takes the StoreOf property of type ProductDomain.

struct ProductDomain: ReducerProtocol {

    struct State: Equatable {
        var addToCartDomainState: AddToCartDomain.State
    }
    
    enum Action {
        case addToCartDomain(AddToCartDomain.Action)
    }
    
    var body: some ReducerProtocol<State, Action> {

       ๐Ÿ‘‡[๐Ÿคจ: Is this how scope is done in Swift?]
        Scope(
            state: \.addToCartDomainState,
            action: /Action.addToCartDomain) {
                AddToCartDomain()
            }
    }
}

This is ProductCard SwiftUI View

struct ProductCard: View {
    let store: StoreOf<ProductDomain>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }, content: { viewStore in
            HStack(alignment: .center) {
                AddToCart(
                    store: store.scope(state: \.addToCartDomainState)
                )
                ๐Ÿ‘†[โ›”๏ธ: this gives an Error]
                ๐Ÿ‘†[๐Ÿคจ: Is this how scope is done in SwiftUI?]
            }
        }
    }
}

Hint: Error description
:warning: Cannot convert value of type 'Store<AddToCartDomain.State, ProductDomain.Action>' to expected argument type 'StoreOf' (aka 'Store<AddToCartDomain.State, AddToCartDomain.Action>')



This is AddToCart SwiftUI view (Child View)

struct AddToCart: View {
    
    let store: StoreOf<AddToCartDomain>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }, content: { viewStore in
           ...
        }
    }
}

This is AddToCartDomain (Child Domain)

struct AddToCartDomain: ReducerProtocol {
    struct State: Equatable {
        ...
    }
    
    enum Action: Equatable {
        case didTapAddToCartButton
        case didTapPlusButton
        case didTapMinusButton
    }
    
    func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
            ...
        }
    }
}

Hi @amith156 ! On ProductCard view body you wanna just call the viewStore.send(.addToCartDomain) action instead call the store.scope() method, that returns a Store type, instead of a StoreOf type as you can see here, and not instantiating any AddToCart state within the view.

hay @otondin thank you for your reply.
sorry, are you telling me to do something like this?

struct ProductCard: View {
    let store: StoreOf<ProductDomain>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }, content: { viewStore in
            HStack(alignment: .center) {
                viewStore.send(.addToCartDomain) <--- ๐Ÿคจ
            }
        }
    }
}

Exactly! Is it the action that matches your scoped case on the reducer from ProductDomain, right?

Scope(
    state: \.addToCartDomainState,
    action: /Action.addToCartDomain) {
        AddToCartDomain()
    }
)

hay @otondin
yes but, AddToCartDomain() is not a view and HStack expects a view

Scope(
    state: \.addToCartDomainState,
    action: /Action.addToCartDomain) {
        AddToCartDomain()
    }
)
๐Ÿ‘†

Sure, you can't instanciate a State object like this either. I think that maybe you're mixing the pieces here. You would need to tap on some UI control component and send some action to your store, in this case, the ProductDomain, then the action will match your addToCartDomain case and instantiate the scope you want to, in this case the AddToCartDomain. Then you need to implement on your view some navigation to your, maybe, AddToCartView.

The .send(...) needs to be inside a context that can run an action, like a Button or .onAppear view modifier, etc...

struct ProductCard: View {
    let store: StoreOf<ProductDomain>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }, content: { viewStore in
            HStack(alignment: .center) {
                Button("Add to Cart") {
                    viewStore.send(.addToCartDomain)
                 }
            }
        }
    }
}
1 Like

sorry, i didn't understand this part, can you please explain with an example?
Thank you!

You can use, for instance, a NavigationStack (if you're targeting your deploy to iOS 16+), and when the user taps on some button send the action to the store, scope your reducer and then effectively navigate to your wanted feature view.

In your view layer, you need to scope not just your state but also your action.

HStack {
  AddToCart(store: store.scope(
    state: \.addToCartDomainState,
    action: ProductDomain.Action.addToCartDomain
  ))
}
1 Like

hay @afarnham, instead of a Button, I want my AddToCart() view within the HStack {...}

@amith156 The post above by @JuneBash has what you want I think.

I see, so you just need to instantiate your AddToCartView. To accomplish that you don't need any AddToCardDomain at all, you can handle all the AddToCart features on your ProductDomain with no scoping needed.

Hmm... I'm not sure what you mean here...

yes but, this returns Store<AddToCartDomain> but i wanted StoreOf<AddToCartDomain>

Store<AddToCartDomain> is not valid code and won't compile.

Store is defined with 2 generic constraints. State and Action. So if defined with a single type like you have done it will not compile.

StoreOf is just a shortcut to Store. Defined so that if you have Store<Reducer.State, Reducer.Action> then you can replace that with StoreOf<Reducer> and they mean the exact same thing.

Can you perhaps add the code that you have and that isn't working. I'm finding it a bit hard to follow what is happening now. :sweat_smile:

Thanks

1 Like

In your code from the OP you should change it to this...

struct ProductCard: View {
    let store: StoreOf<ProductDomain>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }, content: { viewStore in
            HStack(alignment: .center) {
                AddToCart(
                    store: store.scope(
                        state: \.addToCartDomainState,
                        action: ProductDomain.Action.addToCartDomain // <-- change here
                    )
                )
            }
        }
    }
}

Like I said above. Store has TWO generic constraints. When you do store.scope... you are transforming (scoping) those constraints.

In your original code you are only scoping the State of the Store. Which is why you are getting the error. You need to scope the state AND the action.

2 Likes

hay @Fogmeister
thank you, can i please know what use for these two scopes? what are they actually doing?

Sure...

You have two separate contexts happening here. In your original question, in the first code snippet you ask ๐Ÿ‘‡[๐Ÿคจ: Is this how scope is done in SwiftUI?].

But... this is not SwiftUI code. This is just Swift. It might LOOK a bit like SwiftUI but that's because it's built using a ResultBuilder. A deep dive into Swiftโ€™s result builders | Swift by Sundell

Reducer

So... in this code...

struct ProductDomain: ReducerProtocol {
    struct State: Equatable {
        var addToCartDomainState: AddToCartDomain.State
    }
    
    enum Action {
        case addToCartDomain(AddToCartDomain.Action)
    }
    
    var body: some ReducerProtocol<State, Action> {
        Scope(
            state: \.addToCartDomainState,
            action: /Action.addToCartDomain
        ) {
            AddToCartDomain()
        }
    }
}

This is NOT SwiftUI. This is your Reducer. Just written in Swift. There is no UI here. This Reducer called ProductDomain deals with two types.

  1. ProductDomain.State the data part of your reducer
  2. ProductDomain.Action the actions that the reducer can process

BUT...

You have another reducer AddToCartDomain and you need one of these for your other view and your other data and actions.

So we need to get from a reducer of ProductDomain.State and ProductDomain.Action and somehow change this into a reducer of AddToCartDomain.State and AddToCartDomain.Action. To do this... we can Scope it.

So in your code you are passing into it the instructions of how to get from ProductDomain to AddToCartDomain.

  1. You get from ProductDomain.State to AddToCartDomain.State by doing this... \.addToCartDomainState
  2. You get from ProductDomain.Action to AddToCartDomain.Action by doing this... ProductDomain.Action.addToCartDomain

View

In this code...

struct ProductCard: View {
    let store: StoreOf<ProductDomain>
    
    var body: some View {
        WithViewStore(store, observe: { $0 }, content: { viewStore in
            HStack(alignment: .center) {
                AddToCart( ๐Ÿ‘‡
                    store: store.scope(
                        state: \.addToCartDomainState,
                        action: ProductDomain.Action.addToCartDomain
                    )
                )
            }
        }
    }
}

Now you are in the view and this IS SwiftUI. So we have a view that takes a StoreOf<ProductDomain>. BUT we get to a bit further down where we want to display a AddToCart view and that takes a StoreOf<AddToCartDomain. So again... we need to tell the view how to get from StoreOf<ProductDomain> to StoreOf<AddToCartDomain>. How do we do this?

Well, now we can scope the store.

store.scope(
    state: \.addToCartDomainState,
    action: ProductDomain.Action.addToCartDomain
)

This is telling the view how to take one store and scope it to another store.

Summary

For each "scope" you will normally need two parts to match up.

  1. The Reducer is scoped using a Scope(.......) in the reducer body.
  2. The Store is scoped by using store.scope(......) in the view body.

In most basic cases these will look very similar.

4 Likes

thank you @Fogmeister
It kinda makes sense now, but seems like I don't really need this scope in reducer, isn't it?

You need the Scope in the reducer. This does two things. Sort of one going down the chain, and one going back up the chain.

By having this reducer you let TCA do...

  1. Send actions up the chain of Reducers.
  2. Make sure that data changes are correctly passed between parents and children.

Without this scope your app would not function correctly.

For instance you might have code like this in the ProductDomain.State...

struct ProductDomain: ReducerProtocol {
  struct State {
    var addToCartDomainState: AddToCartDomain.State
    var cartItemsCount: Int {
      addToCartDomainState.items.count
    }
  }
}

(This is a contrived example but it's just for illustration purposes).

Without the scoped reducer you will find that any changes to your AddToCartDomain are not reflected back up in the ProductDomain reducer.

A general rule is. Every time you scope the view you need to have a similar scope in the reducer.

3 Likes

Amazing @Fogmeister !!!!!!!! :tada::champagne::partying_face:
thank you for your help and thank you to everyone!!!!

1 Like