SwiftUI & composable architecture (TCA) : how to reduce sub-states?

878 Views Asked by At

I am trying to understand how SwiftUI works with the Swift TCA - The Composable Architecture.

I'm stuck trying to understand how I can use sub-reducers from the main app reducer. Here is what I have written so far:

import ComposableArchitecture

@Reducer
struct AppReducer {
    struct State: Equatable {
        var parentState = ParentReducer.State()
        var childState = ChildReducer.State()
    }
    
    enum Action {
        case parent(ParentReducer.Action)
        case child(ChildReducer.Action)
    }
    
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .child(let childAction):
                state.childState = // how to use child reducer here?
            case .parent(let parentAction):
                state.parentState = // how to use parent reducer here?
            }
            return .none
        }
    }
}

@Reducer
struct ParentReducer {
    struct State: Equatable {
        var count: Int = 0
    }
    
    enum Action {
        case increment
    }
    
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .increment:
                state.count += 1
                return .none
            }
        }
    }
}

@Reducer
struct ChildReducer {
    struct State: Equatable {
        var count: Int = 0
    }
    
    enum Action {
        case increment
    }
    
    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .increment:
                state.count += 1
                return .none
            }
        }
    }
}

@main
struct TestTCAApp: App {
    let store = StoreOf<AppReducer>(
        initialState: AppReducer.State()
    ) {
        AppReducer()
    }
    
    var body: some Scene {
        WindowGroup {
            ParentView(store: store)
        }
    }
}

struct ParentView: View {
    let store: StoreOf<AppReducer>
    
    var body: some View {
        WithViewStore(store, observe: { $0.parentState }) { viewStore in
            VStack(spacing: 16.0) {
                Text("\(viewStore.count)")
                Button {
                    viewStore.send(.parent(.increment))
                } label: {
                    Text("INCREMENT")
                        .font(.largeTitle)
                }
                ChildView(store: store)
            }
        }
    }
}

struct ChildView: View {
    let store: StoreOf<AppReducer>
    
    var body: some View {
        WithViewStore(store, observe: { $0.childState }) { viewStore in
            VStack(spacing: 16.0) {
                Text("\(viewStore.count)")
                Button {
                    viewStore.send(.child(.increment))
                } label: {
                    Text("INCREMENT")
                        .font(.largeTitle)
                }
            }
        }
    }
}

Can you please help me? I have no idea what to do with child and parent reducers. Thank you.

1

There are 1 best solutions below

0
On

Although there are multiple methods, TCA recommends handling the child inside the parent.

So in the parent, you will have a state, action, and reducer for the child (think of it as some proxy):

struct ParentReducer {
    struct State: Equatable {
        var child: ChildReducer.State? // Optional if it is possible for a child to be not activated at some points
    }
    
    enum Action {
        case child(ChildReducer.Action)
    }
    
    var body: some Reducer<State, Action> {
        Reduce { }
            .ifLet(
                \.child,
                action: /Action.child,
                destination: ChildReducer.init
            )
    }
}

Also, you should scope the parent store for the child store and pass it instead:

ChildView(
    store: store.scope(
        state: \.child,
        action: { .child($0) }
    )
)

The case studies examples are a good place to take a look at different scenarios.


⚠️ Be aware that I code this without the compiler and the code may not be ready to just a simple copy-paste