Triggering effects from child #3648
arkulpaKonstantin
started this conversation in
Ideas
Replies: 1 comment
-
I recently had the time to make this more generic and came up with this. There are still several known issues:
One can extend the extension BroadcastStorage {
public var restart: BroadcastListenerEffect<Void> {
get { self[#function] }
set { self[#function] = newValue }
}
public var showDialog: BroadcastListenerEffect<String> {
get { self[#function] }
set { self[#function] = newValue }
}
} Then add the // ...
public var body: some ReducerOf<Self> {
Reduce { state, action in
// ..
}
.listen(for: \.restart, then: .restart)
.listen(for: \.showDialog, then: Action.showDialog)
} and trigger them like this in a child: // ...
@Dependency(\.broadcasts.showDialog) var showDialog
// ...
public var body: some ReducerOf<Self> {
Reduce { state, action in
// ...
return .run { _ in
await showDialog(with: "Hellp")
}
// ...
}
} Code: import ComposableArchitecture
/// An effect that triggers a action on the reducer that listens for it.
///
/// Execute these in the effect returned from a reducer in order to trigger the
/// action in the listening parent reducer:
///
/// ```swift
/// @Reducer
/// struct ChildFeature {
/// struct State { /* ... */ }
/// enum Action {
/// case logoutButtonTapped
/// // ...
/// }
/// @Dependency(\.broadcasts.restart) var restart
/// var body: some Reducer<State, Action> {
/// Reduce { state, action in
/// switch action {
/// case .logoutButtonTapped:
/// return .run { _ in await restart() }
/// // ...
/// }
/// }
/// }
/// }
/// ```
///
/// This operation works by finding the nearest parent feature that uses the
/// `.listen(for: \.restart, then: ParentAction)` operator and sending the specified
/// parent action to that parent reducer.
///
/// ```swift
/// @Reducer
/// struct ParentFeature {
/// struct State { /* ... */ }
/// enum Action {
/// case restart
/// // ...
/// }
/// @Dependency(\.broadcasts.restart) var restart
/// var body: some Reducer<State, Action> {
/// Reduce { state, action in
/// switch action {
/// case .restart:
/// // ...
/// }
/// }
/// .listen(for: \.restart, then: Action.restart)
/// }
/// }
/// ```
///
/// > Warning: The `@Dependency(\.broadcasts.restart)` tool only works for features that are children of parents using
/// > the `listen` operator. If no parent feature is found calling the restart has no effect.
/// >
/// > If you are testing a child feature in isolation that makes use of `@Dependency(\.broadcasts.restart)`
/// > then you will need to override the dependency. You can even mutate
/// > some shared mutable state inside the `call` closure to confirm that it is indeed invoked.
public struct BroadcastListenerEffect<Value>: Sendable {
var call: (@MainActor @Sendable (Value) -> Void)
public init(_ call: @escaping @MainActor @Sendable (Value) -> Void) {
self.call = call
}
@MainActor
public func callAsFunction(with value: Value) async {
self.call(value)
}
@MainActor
public func callAsFunction() async where Value == Void {
self.call(())
}
} import Combine
import ComposableArchitecture
public struct BroadcastListenerReducer<Base: Reducer, Value>: Reducer {
@usableFromInline
let base: Base
@usableFromInline
let keyPath: WritableKeyPath<BroadcastStorage, BroadcastListenerEffect<Value>>
@usableFromInline
let broadcastAction: (Value) -> Base.Action
@usableFromInline
init(
base: Base,
keyPath: WritableKeyPath<BroadcastStorage, BroadcastListenerEffect<Value>>,
broadcastAction: @escaping (Value) -> Base.Action
) {
self.base = base
self.keyPath = keyPath
self.broadcastAction = broadcastAction
}
@inlinable
public func reduce(
into state: inout Base.State, action: Base.Action
) -> Effect<Base.Action> {
let subject = PassthroughSubject<Base.Action, Never>()
let broadcastEffect = Effect<Base.Action>.publisher {
subject
}
.cancellable(id: keyPath, cancelInFlight: true)
let baseEffect = self.base
.dependency(
(\DependencyValues.broadcasts).appending(path: keyPath),
BroadcastListenerEffect { value in
subject.send(broadcastAction(value))
}
)
.reduce(into: &state, action: action)
return .merge([broadcastEffect, baseEffect])
}
}
extension Reducer {
@inlinable
@warn_unqualified_access
public func listen<Value>(
for keyPath: WritableKeyPath<BroadcastStorage, BroadcastListenerEffect<Value>>,
then action: @escaping (Value) -> Self.Action
) -> BroadcastListenerReducer<Self, Value> {
BroadcastListenerReducer(base: self, keyPath: keyPath, broadcastAction: action)
}
@inlinable
@warn_unqualified_access
public func listen(
for keyPath: WritableKeyPath<BroadcastStorage, BroadcastListenerEffect<Void>>,
then action: Self.Action
) -> BroadcastListenerReducer<Self, Void> {
BroadcastListenerReducer(base: self, keyPath: keyPath, broadcastAction: { _ in action })
}
} import ComposableArchitecture
public struct BroadcastStorage {
@usableFromInline
var storage: [String: any Sendable]
init(storage: [String: any Sendable] = [:]) {
self.storage = storage
}
public subscript<Value>(
key: String
) -> BroadcastListenerEffect<Value> {
get {
guard let value = storage[key] else {
return BroadcastListenerEffect<Value> { _ in }
}
return value as! BroadcastListenerEffect<Value>
}
set {
storage[key] = newValue
}
}
}
extension BroadcastStorage: DependencyKey {
public static let liveValue = BroadcastStorage()
public static let testValue = BroadcastStorage()
}
extension DependencyValues {
public var broadcasts: BroadcastStorage {
get { self[BroadcastStorage.self] }
set { self[BroadcastStorage.self] = newValue }
}
} |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
We are currently working on an App that needs to trigger some functionality in the root reducer from many child reducers and child of child of child ... reducers.
To handle this we used to listen to all the child actions and then trigger the functionality, but as that got really confusing and a lot of boilerplate for every new child action we added something like the
dismiss
functionality for this case.I thought I would share this and suggest to make it more general with some hashable ID or so.
One issue we ran into is that in tests the effect is expected to finish. But one can not for now do that so we have to use
skipInFlightEffects
.Here is our code. (As you may guess the functionality we need has to do with the parent reducer doing a restart 😅)
The effect:
The reducer modifier:
The root reducer then just adds this line:
Then then all the children can use the dependency like so:
and simply restart the root reducer by returning a run effect that calls the restart:
Beta Was this translation helpful? Give feedback.
All reactions