Comments (6)
Hey Maxim, it actually is possible to cancel effects in the composable architecture, and can be quite nice.
Here's a gist of how to enhance an effect with the capabilities of being cancellable, as well as a new effect that cancels an inflight effect:
https://gist.github.com/mbrandonw/1c4ca76f1927420921b3871dd2153497
(also a fellow Point-Free subscriber @alexito4 made a nice video on using this code http://youtube.com/watch?v=VAB3lysXU9o)
This allows you to do something like this:
let reducer = Reducer { state, action, environment in
struct LoadUserId: Hashable {}
switch action {
...
case .reloadUserButtonTapped:
return [
.cancel(id: LoadUserId()),
environment.loadUser()
.map(UserProfileEvent.userResponse)
.cancellable(id: LoadUserId())
]
case .sceneShown:
...
return environment.loadUser()
.map(UserProfileEvent.userResponse)
.cancellable(id: LoadUserId())
...
}
}
Now if sceneShown
happens and then immediately the user invokes reloadUserButtonTapped
, the inflight user request will be canceled and a new one will be started.
The private repo we shared with you yesterday has a more robust version of this effect cancelation if you want to check it out. And it's also possible to implement this kind effect cancellation in RxSwift if that's the library you are using.
from episode-code-samples.
I think that Composable Architecture has two significant advantages over RxFeedback. The first one is the machinery to decompose state and reducers and then combine them back together. RxFeedback has no such tools, and even without side-effects inside reducers, you end up with huge complex reducers.
The second one is the Elm's approach to side-effects. With RxFeedback it's very hard to reason about what side-effect will be triggered if you change somehow the state because with queries it is implicit. It manifests itself pretty badly when you are working with the unfamiliar codebase. But in Composable Architecture it's made explicit, and it's just obvious from a quick look at the reducer.
@atimca if you are interested I made an adapter to convert reducers from RxFeedback to Composable Architecture shape. It can help to adopt incrementally the Composable Architecture.
https://gist.github.com/ixrevo/77c86ea4c762ec2ef91c07a4e2007743
from episode-code-samples.
Hey @atimca, thanks for the detailed issue! There's a lot of content here, so we'll try to respond to as much as possible.
The RxFeedback approach is very good, and if you get a lot of mileage out of it that's great!
Here's a few responses to your bullet points:
-
Less code in one place. Simple changes for the state over reducer and one service call in side effects
While true, on the flip side it also means more code in more places 😬. We think it can be powerful to have fewer places to look to figure out what the application is doing, and seeing state changes and effects side-by-side is very convenient. And we mitigate the potential for complexity by using tools like
combine
andpullback
, which help break large reducers down into small, succinct ones. -
Bigger separation from the state itself and actions (If we introduce query-based side effects, the state should always reflect "state" of the app) Right now it's possible to perform a side effect only based on action.
Executing side-effects only based on actions hasn't yet been problematic for us, and we've used this style of effects in quite a few apps in production. Can you think of things you can do with that style of effects that is not possible with our style?
-
Better testability. It's possible to test reducer separately from actions + actions are tests like a store (system) of all blocks together
It seems testability is the same. You can always ignore the effects when testing the reducer directly, as we did here. But also, with the test helper developed here it doesn't seem as useful to test state mutation separate from effects. You get the most comprehensive tests by considering them together since they can influence each other in subtle ways.
-
Migration between architectures. When all pieces are separate it's much easier to go from rxfeedback to redux or even mvvm if needed
It seems that migration between architectures is always going to be difficult, no matter the style. Also, we do not believe that RxFeedback is more "separated" than the composable architecture. RxFeedback puts the code in different locations/files, but the state mutation code and the effect code in the composable architecture is still 100% separated from each other. You can move all effect code into private helper functions if you don't want that code literally inside the reducer.
-
Reducer doesn't know of any dependencies via the Environment approach or any other. It seems more legit to call reducer a pure function without any dependencies around, which could influence result of reducer as an array of side effects.
Since Swift is incapable of expressing pure functions it can be difficult to pin down exactly what is pure and impure. As far as state mutations go, our reducers are pure in that the state change is completely determined by the initial state and action. And the construction of the effects is completely determined by the state of the
Environment
passed in. So it's pure enough for us to get a great deal of understanding of our code, but at the end of the day nothing stops us from doing something impure if we so desire (same as in RxFeedback).
All of the points you bring up are completely valid, and RxFeedback has prioritized certain things higher than we have, and we have our own priorities too.
Now more concretely, here are some things I've noticed in the code you posted.
There are a lot of steps to take to figure out how effects are executed since they are kept far apart from the reducer. For example, to know when a user is loaded I first have to understand this:
react(request: ^\.queryLoadUser, effects: loadUser)
which is triggered when this computed property becomes non-nil
:
var queryLoadUser: Void? {
(user == nil) ? () : nil
}
which then I have to look at all places in the reducer where the user
could be set to nil
. It happens initially, but it could also happen in other places.
Alternatively, we could have the following type of reducer:
let reducer = Reducer { state, action, environment in
switch action {
...
case .sceneShown:
...
return environment.loadUser()
.map(UserProfileEvent.userResponse)
...
}
}
It's now all in one place, and I know precisely when the user is loaded. If later another action was introduced for "reloading" the user, we could simply do:
let reducer = Reducer { state, action, environment in
switch action {
...
case .reloadUserButtonTapped:
return environment.loadUser()
.map(UserProfileEvent.userResponse)
case .sceneShown:
...
return environment.loadUser()
.map(UserProfileEvent.userResponse)
...
}
}
Further, because effects are only driven by state in RxFeedback, and not actions, you have to keep around extra state that you would not typically. For example, the eventsToLog
state is not intrinsic to your feature, you only have it around so that you can execute a side effect later, and you have to send another action later to clean up those logs.
However, if you created effects in the reducer you could just do that work right along side loading the user:
let reducer = Reducer { state, action, environment in
switch action {
...
case .sceneShown:
...
return [
environment.logEvent(.pageView()),
environment.loadUser()
.map(UserProfileEvent.userResponse)
]
...
}
}
Notice that it doesn't even need to feed information back into the reducer, it's just a fire-and-forget.
So, it is actually possible to delete quite a bit of code by returning the effects right in the reducer, and you get to have all of your business logic in one place instead of spread in multiple places.
And finally, the part I'd be more interested in seeing in the RxFeedback-style is composition. We get a lot of power out of transforming reducers and Store
s. It allows us to break down our applications into small modules, which the glue together to form the entire application. I'm certain that can be done with RxFeedback, but it'd be nice to see what it looks like and how the ergonomics compare with the composable architecture.
It'd also be interesting to see how your code sample would look when converted to the composable architecture style to get a fair, side-by-side comparison. If you are interested in doing that I'd be willing to help or answer questions to the best of my ability.
Thanks again for writing up this super detailed issue. I believe we can do a better job in the future to describe why we have made the choices we have made in designing the composable architecture.
from episode-code-samples.
Thank you for the fast answer @mbrandonw ! All your points are valid and from my point of view, I see composable architecture more supported, than RxFeedback. There's no store concept in RxFeedback and no integrated way to modularize things. I used to have Store for RxFeedback components before saw your videos, just for integration with legacy code. However, after your videos, I managed the more elegant way to separate modules with pullbacks and view functions ❤️.
Seams like calling side effects based on actions could really save some annoying code. If you need to download something, by pressing a download button, maybe better to do it, without trying to adapt state for it. However, this state-based approach helped me to construct a state in a more simple and "State machine" way. IMHO perfect state could be expressed as an Enum. I'll put an example below.
public enum SignState: Equatable {
case appleSignedIn(Apple.Token)
case error(SignError)
case facebookSignedIn(Facebook.Token)
case googleSignedIn(Google.Token)
case initial
case signOut
case signed
case started(SignInType)
case toSignIn
case toSignUp
}
One point for query-based side effects that they look more reactive. If just create side effects based on action it could provide more actions like "downloadWasTapped" than just create an effect for download. Easier to manage, but maybe not the best way to fill the approach. However, don't want to separate ways of doing thing on right and don't right. Both of the approaches seem valid for me, just trying to understand which one is more suitable for me.
The cool point about all this, that I could try composable architecture in my project with existing RxFeedback modules. And migration from one to another is quite easy! However, It's only possible with Rx based implementation, since my support target is iOS 12. So, looking forward when you finally open-source it and I hope I can be a part of creating Rx version who those, who cannot use iOS 13 for now)
from episode-code-samples.
One point for query-based side effects that they look more reactive
This is a very good point. Having everything in the reactive chain means you can do some complex things quite simply, like if you wanted to debounce the downloadTapped
you can just do downloadTapped.debounce(0.3)
and you're done.
You can recover something like this in the composable architecture, and it's quite similar, but a little different. We hope to talk about it soon.
from episode-code-samples.
I hope you don't mind if I will add something here when remembering new stuff. So, with a query-based side effects, you can easily cancel the effect if the query does not fulfill the feedback.
var queryLoadUser: Void? {
(user == nil) ? () : nil
}
According to this one, if the user was gathered from somewhere else this effect will be canceled. It's not so bad to reload user several times, except some performance/networking issues. However, right now for composable architecture, there's no way to cancel an event, if I fully understand. Sometimes not the canceling effect could be destructive for the app, because it would perform an action, which mutate the state, which leads to an unpredictable state in the end.
from episode-code-samples.
Related Issues (20)
- 0167 Navigation is weird on iPad HOT 1
- Please fix the error Episode #76 ~ HOT 2
- [0171-modularization-pt1] Setting Up HOT 3
- modularization folders architecture HOT 1
- Missing episode 187: URL Route? HOT 1
- Cannot convert value of type - Examples will not compile using Xcode 13.3 HOT 2
- Request: Re-Record Episode 100 to 103 and Update Source Code HOT 3
- Episode 196: Runtime warning work only in half of the cases HOT 1
- minor bug in episode 198 sample HOT 1
- Code sample for episode 220 is missing HOT 1
- Strange navigation on episode 220 HOT 1
- Episode 2 -> Fish playground -> Ambiguous use of 'init(utf8String:)'
- Episode 0228 - Composable Navigation: Destinations sample code not found HOT 1
- navigationDestination not triggered HOT 3
- Modern SwiftUI: Standups App HOT 1
- Episode 241 code not published HOT 2
- Episode 109 sheet show twice when item name changed HOT 1
- Typo in Navigation Example code
- @FocusState not working with new @BindableState API HOT 5
- UIApplication.shared.windows deprecated in iOS 15 HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from episode-code-samples.