Comments (5)
Thank you so much again for your so detailed explanation of possible ways to deal with my unusual cases. Now I have answers for all my questions, and the issue can be closed.
from swiftrex.
Hi!
In SwiftRex currently there's no options to dynamically add or remove things from the store. I wrote this middleware some time ago (https://github.com/SwiftRex/GatedMiddleware) to explore the option to disable or enable middlewares dynamically, either by receiving an action or after certain state matched certain condition. I used to disable analytics, or disable debuggers for certain builds, but to be very honest I think there are better ways of doing that. The Middleware should know what's relevant for itself and ignore all the rest. Same for reducers.
Big part of ignoring irrelevant actions comes from the lifting, as you well mentioned. Let's say you have 2 forms written in 2 different modules (SPM packages, for example) and a main module that groups them all. Because this 2 forms are completely different business logic, very isolated, your Action (and also State) structure will also be different branches in your tree, like this:
// Main Target
import Form1
import Form2
enum AppAction {
case .form1(Form1Action)
case .form2(Form2Action)
}
// Form1 Framework
enum Form1Action {
case list, delete, insert, update
}
// Form2 Framework
enum Form2Action {
case list, delete, insert, update
}
Your middlewares + reducers will focus on FormNAction and ignore anything else. In fact, making them generic over FormNAction will force you to lift them to AppAction (using the \AppAction.form2 prism). The lift won't forward form1 actions to form2, or vice-versa, even because these modules don't even know the other action.
// Form1 Framework
class SomeMiddleware: MiddlewareBase<Form1Action, Form1Action, Form1State> { ... }
// Main Target
let mainMiddleware = Form1.SomeMiddleware()
.lift(
inputAction: \AppAction.form1,
outputAction: AppAction.form1,
state: \AppState.form1
)
Once your store receives a form2 action, the lift for input action will try to extract the branch \AppAction.form1 out of it, which returns nil (thanks to the prism getter), so the action will never be forwarded to the Form1.SomeMiddleware.
When the modules share some business logic, you will have to ignore actions using business logic, there's no other way. The GatedMiddleware was a generic attempt to do that, but you don't actually need it. Let's say, your OnboardingMiddleware will handle all sorts of actions and it's only relevant while the user is performing the Onboarding on your app for the first time. After that, you want it to ignore actions.
Well... In that case, somewhere in your app you will need a piece of state .isOnboardingFinished that will be set by the OnboardingReducer (and maybe even persisted to UserDefaults/CoreData/whatever by your OnboardingMiddleware, and restored every app launch). In your Middleware's func handle(action: AppAction, from dispatcher: ActionSource, afterReducer: inout AfterReducer)
, the first thing you should do is to read getState().somewhere.isOnboardingFinished
and in case you receive true
, you bypass that middleware.
The cost is very very low.
Form1Middleware and Form2Middleware are never destroyed. You don't have to. If you run processes that you want to cleanup, for example a timer that fetches certain data from the web, or a websocket connection, or whatever, and you want this at some point to be cancelled, you must receive an action like ".moduleForm1IsFinished" and act on that, usually cleaning up your cancellables (self.cancellables = Set()
/ self.disposeBag.dispose()
) which will destroy every process ran by the middleware. That could also be reduced in the state so whenever moduleForm1IsFinished action arrives, you change certain flag in your state and Form1 Middleware will read this before any task to ensure it's still alive.
I don't see a use case for when you really want to remove the middleware from the store. But if this is what you really want, you can have a middleware container that is dynamic, and eventually sets its internal middleware to nil.
Imagine this:
https://github.com/SwiftRex/SwiftRex/blob/develop/Sources/SwiftRex/Middlewares/LiftMiddleware.swift#L16
where part middleware can be set to nil. It's possible, I just honestly think it's not the best way because it requires a lot of logic that can fail. IMHO it's better to simply ignore actions and logically cancel subscriptions.
Please tell me if I answered your question or there are cases that I don't see here.
from swiftrex.
Thank you so much for such detailed explanation!
You are suggesting good way to work with apps where all modules (view + data) are different.
But what can you advice for a situation when one module may have several instances? For example, AppModule shows FormModule, and then FormModule can create another instance of FormModule with different data, and so on.
Let's imagine that SwiftRex has possibility to add/remove reducers/middlewares on-fly. Then I can have such structures:
struct AppState {
someInfo: String,
form: FormState?
}
class FormState {
field1: Int,
field2: String,
form: FormState?
}
- When the app is launched, it creates the global store (using hypotetic DSL):
store = store(app.reducer, app.middleware)
- When the app creates and shows FormModule N1 it does something like this:
store.reducer.append(form1.reducer.liftedToApp)
store.middleware.append(form1.middleware.liftedToApp)
- And now, when Form N1 creates and shows Form N2:
store.reducer.append(form2.reducer.liftedToParentForm)
store.middleware.append(form2.middleware.liftedToParentForm)
In both 2) and 3) steps Action and State can be easily converted from/to parent ones.
Removing reducers/middlewares on module destruction also works well in this fantasy.
But SwiftRex doesn't support possibility to add/remove reducers/middlewares on-fly. In this situation, when Form sends an Action, how reducer/middleware could recoginize which Form instance sends it - N1 or N2? I see following decision:
struct AppState {
someInfo: String,
forms: [String : FormState]
}
struct FormState {
field1: Int,
field2: String
}
enum FormAction {
case tapEdit(String)
}
AppState should track every Form instance by some identifier (String
dictionary key), and every FormAction should contain the identifier of the Form that sends it, so reducer/middleware can extract correct FormState.
Are there more elegant ways exist to deal with such situations?
from swiftrex.
TL;DR:
You AppState doesn't need the dictionary or array of Forms, only the entry-point (root) of your tree. With identifiers you should be able to traverse the tree from the middleware/reducer directly or using lift.
Down the rabbit hole:
So, I wouldn't indeed use nested Middlewares because of nested state. Or reducers. If Form1 has a Form1 as child, you're still in the realm of Form1 business and imho the Form1Middleware has the logic to deal with Form1 knowing that it may have a child of type Form1 as well.
I have similar recursive state in one of my apps. In that case, we have a content browser, so imagine you are browsing Spotify, then you tap Rock, then 60s, then Beatles, then the White Album and then finally you tap a song that plays in your speakers. This structure is nothing but a content tree, which I have a recursive state. For me, the perfect type to represent trees are enums. So imagine an enum like:
public enum ContentBrowsingTree: Hashable {
case root(contentID: String)
indirect case node(contentID: String, parent: ContentBrowsingTree)
/// Prints the content ID for the current node in the tree
public var nodeId: String {
switch self {
case let .node(nodeId, _): return nodeId
case let .root(rootId): return rootId
}
}
/// Given a content ID, finds exactly the node where this content object is present in this tree
/// or nil in case the contentID is not part of this tree.
/// - Parameter contentID: some content ID to search in this tree
/// - Returns: the tree, from the root to the content object provided, or nil if the content ID
/// is not part of this tree
public func node(for contentID: String) -> ContentBrowsingTree? {
switch self {
case let .node(nodeId, parent):
if nodeId == contentID { return self }
return parent.node(for: contentID)
case let .root(contentID):
if contentID == contentID { return self }
return nil
}
}
/// Given a content ID, finds a node with that content object ID, and then its immediate child.
/// If the content ID is not found, or it's found but it's a leaf node, then nil will be returned.
/// - Parameter parentId: a content object ID from the parent of the node we are searching for.
/// - Returns: a child of certain container, if found, or nil if the container is leaf node or the
/// ID is not found
public func child(of parentId: String) -> ContentBrowsingTree? {
switch self {
case .root: return nil
case let .node(_, parent):
if parent.nodeId == parentId {
return self
}
return parent.child(of: parentId)
}
}
}
You need an identifier for each level. The root could be "/" then from that you can build something like "/Genre/Rock/Category/60s/Artist/Beatles/Album/TheWhiteAlbum"
Your reducer and middleware have a way to find precisely the content as if it was a flat structure, but work in a tree structure whenever needed. This is also helpful for views, because in my case I use NavigationLink to show all possible children of certain content container, that view has a view state set to nil when no children is selected, but once a child is selected I use the ContentID (which is hashable) in the Binding for the NavigationLink, which makes the NavigationView to push the child. the Navigation is also recursive, and you can have as many levels as you need. ContentList -> NavigationLink to destination ContentList -> NavigationLink to destination ContentList -> NavigationLink to destination ContentList ... etc etc etc.
My middleware understands Content Browsing, not content browsing for the root directory, or content browsing for album A.
If this was the case, you could also lift using the content ID. Using those helpers in the ContentBrowsingTree you should be able to fetch the immediate child, or the leaf node, the root node, the parent, etc. With that, you could identify your action based on the content tree.
Here you find some ways to use lifting with Identifiable elements in a collection, to lift single element within an array:
https://github.com/SwiftRex/SwiftRex/blob/develop/Sources/CombineRex/EffectMiddleware%2BLiftToCollection.swift#L34
https://github.com/SwiftRex/SwiftRex/blob/develop/Sources/SwiftRex/CoreTypes/Pipeline/Reducer%2BLiftToCollection.swift#L5
For collection of non-Identifiable, through index, it's possible as well. You can use that idea to make a lift that works in a tree structure, if this is what you look for. The View part of that can also use projection with the same purpose, transform a Store Projection that is a collection of elements, into a Store Projection that is one element in that collection:
https://github.com/SwiftRex/CombineRextensions/blob/master/Sources/CombineRextensions/ForEach%2BExtensions.swift#L55
I guess you can use similar idea for trees as well. Every tree can be turned into a flat array as long as you make them identifiable somehow. So if your Form has hierarchy, you need to track somehow where in the Form you are.
One last thing to mention. If you follow the links to the LiftToCollection middleware, you will see that I rely on a special type of action:
public struct ElementIDAction<ID: Hashable, Action> {
public let id: ID
public let action: Action
public init(id: ID, action: Action) {
self.id = id
self.action = action
}
}
This is a way to associate actions of individuals to action of collections.
You could have:
public typealias ContentScopedAction = ElementIDAction<ContentID, ContentItemAction>
You ContentItemAction would be "tapEdit, tapSave, delete" without the need of associated values in these enum cases. Then you scope this enum to a ContentScopedAction containing the proper identifier. Remember:
enum X {
case a(String)
case b(String)
case c(String)
}
// is algebraically the same as:
struct Scope {
let id: String
let y: Y
}
enum Y {
case a, b, c
}
The second is more clean: n * a + n * b + n * c == n * (a + b + c)
.
Please let me understand better your use case and maybe we can find a good solution for your case.
from swiftrex.
I'm glad to hear that you've got your answers. But in any case if you find ways to improve the API please let me know and we can reopen the issue and discuss a bit more.
from swiftrex.
Related Issues (20)
- middleware lift with WritableKeyPath doesn't work on AnyMiddleware HOT 2
- Fix animations from Middleware
- Dead-Letter Queue HOT 1
- Memory leak in LiftMiddleware
- When will develop be the official release? HOT 2
- Lift EffectMiddleware using KeyPath not receiving context
- How to modify AppLifecycleMiddleware to adopt the new MiddlewareProtocol? HOT 2
- Redundant conformance of 'ASAPScheduler' to protocol 'Scheduler'
- Using await/async in middleware HOT 4
- IO monad and stack limit HOT 6
- I think it's time to enable Enabling Concurrency warnings in Xcode HOT 2
- Migrate away from Travis CI HOT 4
- Design question : Model to State migration HOT 2
- Dispatch actions and run reducers on background thread? HOT 4
- CombineRex : Unexpected PublisherType behavior HOT 3
- Trying to use Effect.promise with Firebase's DynamicLinkComponents.shortenURL HOT 5
- Question about assertionFailure() HOT 5
- How organize communication between different states HOT 2
- Improve Tests
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 swiftrex.