pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.35k stars 1.44k forks source link

onChange not work for state change by setter #2488

Closed zpbc007 closed 1 year ago

zpbc007 commented 1 year ago

Description

onChange not work for state change by setter.

Checklist

Expected behavior

onChange should trigger when the parentNumber changed

Actual behavior

onChange only trigger when the parentNumber changed by ParentFeature,when ChildFeature change it's number, ParentFeature.State.number changed but onChange not fire

Steps to reproduce

struct ChildFeature: Reducer {
    struct State: Equatable {
        var number: Int
    }

    enum Action: Equatable {
        case addNum
    }

    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
        case .addNum:
            state.number += 1
            return .none
        }
    }
}
struct ChildView: View {
    let store: StoreOf<ChildFeature>

    var body: some View {
        WithViewStore(self.store, observe: { $0 }) {viewStore in
            HStack {
                Text("child num: \(viewStore.number)")
                Button("+") {
                    viewStore.send(.addNum)
                }
            }
        }
    }
}

struct ParentFeature: Reducer {    
    struct State: Equatable {
        var parentNumber: Int

        var childState: ChildFeature.State {
            get {.init(number: parentNumber)}
            set {parentNumber = newValue.number}
        }

        var latestInfo = ""
    }

    enum Action {
        case add
        case child(ChildFeature.Action)
    }

    var body: some ReducerOf<Self> {
        Scope(state: \.childState, action: /Action.child) { 
            ChildFeature()
        }

        Reduce {state, action in
            switch action {
            case .add:
                state.parentNumber += 1
                return .none
            case .child:
                return .none
            }
        }
        .onChange(of: \.parentNumber) { oldValue, newValue in
            Reduce { state, action in
                state.latestInfo = "oldValue: \(oldValue), newValue: \(newValue)"
                return .none
            }
        }
    }
}
struct ContentView: View {
    let store: StoreOf<ParentFeature>

    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            VStack(alignment: .leading, spacing: 10) {
                Text("latestInfo: \(viewStore.latestInfo)")

                HStack {
                    Text("Parent num: \(viewStore.parentNumber)")
                    Button("+") {viewStore.send(.add)}
                }

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

The Composable Architecture version information

No response

Destination operating system

No response

Xcode version information

No response

Swift Compiler version information

No response

stephencelis commented 1 year ago

@zpbc007 The onChange modifier only applies to the reducer you chain onto it with. So in this case. the Reduce. If you want it to apply to the all of the child reducers, you must first combine them using CombineReducers (or in their own conformance/builder):

+CombineReducers {
   Scope(state: \.childState, action: /Action.child) {
     ChildFeature()
   }

   Reduce {state, action in
     switch action {
     case .add:
       state.parentNumber += 1
       return .none
     case .child:
       return .none
     }
   }
+}
 .onChange(of: \.parentNumber) { oldValue, newValue in
   Reduce { state, action in
     state.latestInfo = "oldValue: \(oldValue), newValue: \(newValue)"
     return .none
   }
 }

Since this isn't a bug with the library, I'm going to convert it to a discussion.