Dynamic dependencies #1287
Replies: 11 comments 37 replies
-
This is great and something I was going to ask too until I saw the topic already raised. I'll definitely be exploring some of these ideas and report back. Thanks 👍🏻 |
Beta Was this translation helpful? Give feedback.
-
The dynamic leaf approach looks like it can be "exploited" to make pieces of state travel through the environment in a standardized way. For example, with some // From a Root reducer
var body: some ReducerProtocol {
Observe { state, _ in
TruncReducer()
.dependency(\.parcel, state.parcel)
}
}
// In a Branch reducer:
struct Branch: ReducerProtocol {
@Dependency(\.parcel) var parcel
var body: some ReducerProtocol {
LeafReducer(parcel: parcel)
}
}
// In Leaf reducer:
struct Leaf: ReducerProtocol {
let parcel: Parcel
func reduce(state: S, action: A) -> Effect<A, None> {
switch action {
case .onAppear:
state.parcel = parcel
return .none
}
}
} Maybe this can be formalized into something more compact. For example, one can imagine the |
Beta Was this translation helpful? Give feedback.
-
Okay great, this looks like it covers my use case. I realize that gradually adding TCA into a legacy code base for which you might never make it to the point where you have a root reducer is not ideal but it's good to see that it's still supported. So far many of the dependencies my TCA feature clients might depend on are not themselves broken out into separate modules. I'm working at modularizing but the process can be a bit slow at times. |
Beta Was this translation helpful? Give feedback.
-
Looks like this can also replace something I've been doing with non-beta TCA. I have a |
Beta Was this translation helpful? Give feedback.
-
Hey @stephencelis , I'm just trying to implement the Root-level configuration method you described above but I think I'm still missing something. Referring to this code here: let apiClient = APIClient(configuration: ...)
let smallFeatureClient = SmallFeatureClient(apiClient: apiClient)
Store(
initialState: AppReducer.State(),
reducer: AppReducer()
.dependency(\.apiClient, apiClient)
.dependency(\.smallFeatureClient, smallFeatureClient)
) I can see how you create the dependencies and then assign them when initializing the extension DependencyValues {
var speechClient: SpeechClient {
get { self[SpeechClientKey.self] }
set { self[SpeechClientKey.self] = newValue }
}
}
enum SpeechClientKey: LiveDependencyKey {
static let previewValue = SpeechClient.lorem
static let testValue = SpeechClient.unimplemented
} Here it looks like the keys are statically assigned but how would that work with a dynamic dependency? |
Beta Was this translation helpful? Give feedback.
-
Okay I think I see how this accomplished. In my extension DependencyValues {
public var smallFeatureClient: SmallFeatureClient {
get { self[ManageSiteClientKey.self] }
set { self[ManageSiteClientKey.self] = newValue }
}
private enum SmallFeatureClientKey: DependencyKey {
static var liveValue = SmallFeatureClient.unimplemented
static let previewValue = SmallFeatureClient.unimplemented
static let testValue = SmallFeatureClient.unimplemented
}
} And then later created the store like this: let store = Store(initialState: initialState,
reducer: SmallFeature()
.dependency(\.smallFeatureClient,
SmallFeatureClient(outsideClient: myOutsideClient))) And that seems to be getting the job done |
Beta Was this translation helpful? Give feedback.
-
Thank you @stephencelis for describing some of the alternative approaches for managing dependencies. Just like we try to make invalid states impossible to represent for example with better use of sum types, I see a parallel in the definition of child environments where their construction at compile-time guarantees the availability of certain services in the reducer. Leaning into the As an example, a network dependency of an authenticated user at the application’s entry point cannot have a meaningful default value and should only exist past the authentication flow. The “dynamic leaf configuration” approach seems an excellent way to avoid this problem, and I wonder if you have any thoughts on how this problem could be prevented using Maybe the answer is simply that there are different approaches available and we should choose the right one for the job at hand, but I am nonetheless curious to hear your stance on this issue. |
Beta Was this translation helpful? Give feedback.
-
I'm having an interesting failure on Xcode Cloud. Everything builds/runs/tests locally but on Xcode Cloud CI. Error from build logs. Not sure if it's Beta issues or not. Started happening after using DependencyValues.
|
Beta Was this translation helpful? Give feedback.
-
Great thread. I have some past experience passing dependencies to other dependencies in SwiftUI.Environment and some thoughts how it might look in TCA. However, I have a problem with one of the premises of this discussion. @stephencelis, in your original post, you explore ways to pass an There are downsides to having this "full access":
I wonder whether or not folks think it would be desirable to have full access to all |
Beta Was this translation helpful? Give feedback.
-
It's a real good discussion, but I have some question what approach is better? :-) Case 1:
Is it a good approach to use my dependency from @dependency? I think in this way: Case 2:
|
Beta Was this translation helpful? Give feedback.
-
With Swinject, we could define assemblies for different scopes, and then based on the context those assemblies could be applied. Example: We have an application that requires dependencies to be in these modes:
This is an example of the entry point of the app that requires Is there any simple and convenient way to introduce demo mode or switch it during the runtime other than overriding every service manually? I can imagine forking the dependency library and introducing a |
Beta Was this translation helpful? Give feedback.
-
In announcement thread, @tylerjames asked a great question about using the new style with more "dynamic" dependencies.
Root-level configuration
Before introducing the reducer protocol, we typically described the "environment" of the reducer as a static set of dependencies for your application that is supplied at launch. This meant that dependencies that depended on one another needed to be assembled and pieced together carefully in order at the app's entry point.
You can still do this kind of work by assembling dependencies that need some dynamic configuration at launch, and then invoking
ReducerProtocol.dependency(_:_:)
on the reducer you pass to the store:Slicing large dependencies into smaller endpoints
The new style also makes it easier to explore alternatives, though!
For one, if
SmallFeatureClient
is just a slice ofAPIClient
that forwards a few endpoints to the feature, you could consider giving the featureAPIClient
and expanding key paths to those specific endpoints directly in the reducer:We can think of
@Dependency
use as mostly "static" still: you can access part of a static value that was supplied at the root of the application, but may have been transformed in some way by a parent viaReducerProtocol.dependency
.While you might worry about exposing the entire API client to this small feature, we think that clients should be super lightweight interfaces that compile almost immediately, and if the live dependency is heavyweight, it can be separated into its own module that the feature does not depend on.
Dynamic leaf configuration
There's also the idea of a "dynamic" dependency: a dependency that relies on some configuration that may differ depending on where the reducer is installed in the reducer tree, or some other dynamic value (in the environment or even state). For example, maybe you have a generic reducer that can render any kind of "timeline" data so long as it can fetch a timeline. Rather than use
@Dependency
, you would let the reducer be configurable with a local dependency that the parent can supply:Then a parent could configure this using a more static global:
"Computed" dependency values
It's also possible to expose dependency values that are constructed out of other dependencies:
Managing computed dependencies come with the same gotchas as computed state: you need to be careful about synchronization, and it can be verbose, but once everything is glued together it will let you avoid pushing this synchronization to the app entry point.
We haven't explored this too deeply, but wanted to share the idea.
These are just a few initial thoughts and ideas! Feel free to try them out or share your own 🤠
Beta Was this translation helpful? Give feedback.
All reactions