by

Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Activity
    Jonah Neugass
    @bgntsty_twitter
    So I've created a protocol that wraps the middleware process function so that I can inject dependencies into my middleware and I'm thinking about doing something similar to my reducers. Are there any reasons why this is a bad idea? Here's the protocol and and example of how the wrapper is being used:
    public protocol MiddlewareContainer {
        func process() -> Middleware<StateType>
    }
    
    public struct RegionChangeMiddleware: MiddlewareContainer {
        private let configuration: Configuration
    
        public init(configuration: Configuration) {
            self.configuration = configuration
        }
    
        public func process() -> Middleware<StateType> {
            return { dispatch, getState in { next in
                return { event in
                    if let regionEvent = event as? SetRegionEvent, let region = regionEvent.region {
                        self.configuration.setCurrentRegion(region)
                    }
                    next(event)
                }
            }
            }
        }
    }
    Malcolm Jarvis
    @mjarvis

    I see nothing wrong with the middleware structure you have there. Having the middleware function be on a struct or object type is a common pattern.

    I don't, however, understand how this would be helpful to reducers? Reducers should not have dependencies, they should be pure, simple functions with next to zero logic.

    Jonah Neugass
    @bgntsty_twitter
    @mjarvis Well, for authentication action processing for instance, we need access to our SSOManager which has information on the state of our SSO session which is needed to determine the new AuthenticationState. There is an option of adding the dependency to every object that sends Authentication events, or adding it just to the resolver container. Note that I am working on refactoring someone else's implementation and trying to limit the amount of refactoring.
    Malcolm Jarvis
    @mjarvis
    that should be handled by middleware, not in a reducer. the middleware should translate + dispatch a pure action for the reducer to handle without dependencies
    donatimariano
    @donatimariano

    Hello there. I wanted to ask if you follow any pattern within middlewares in order to reduce the amount of code they contain. I have several middlewares, where each one of them intercept several actions. The code on each case statement sometimes is around 4 to 7 lines. Even they are easy to read, I'm just afraid of getting these files too big that they become hard to maintain. Are you following any practice to reduce code in middlewares? Would managers/helper classes make sense?

    On a different topic I also wanted to ask if computed properties are OK to have in states or that smells...

    Dario
    @Diarrhio
    @donatimariano I'm relatively new to Redux, so my approach may not be following best practices. The approach I took was to have an accompanying class for each middleware which did everything that the middleware would, with the middleware just acting as a router to call the accompanying methods on the class.
    Malcolm Jarvis
    @mjarvis
    @Diarrhio Thats the pattern I've used as well.
    donatimariano
    @donatimariano
    Thanks guys. I assume that's a class with static methods only? What do you think about encapsulating the whole middleware on a class? Having a base middleware class with concrete classes overriding the relevant method.
    Jonah Neugass
    @bgntsty_twitter

    @donatimariano We've created a MiddlewareContainer protocol that I use to wrap our middleware.

    public protocol MiddlewareContainer {
        func process() -> Middleware<StateType>
    }

    This allows for DI in our middleware:

    import ReSwift
    
    public struct RegionChangeMiddleware: MiddlewareContainer {
        private let configuration: Configuration
    
        public init(configuration: Configuration) {
            self.configuration = configuration
        }
    
        public func process() -> Middleware<StateType> {
            return { dispatch, getState in { next in
                return { event in
                    if let regionEvent = event as? SetRegionEvent, let region = regionEvent.region {
                        self.configuration.setCurrentRegion(region)
                    }
                    next(event)
                }
            }
            }
        }
    }
    @mjarvis With all business logic essentially supposed to be stored in middleware, how does this scale for large scale, complex apps. I would imagine you could end up with tons of middleware...
    Dario
    @Diarrhio
    @donatimariano I took a different approach to @bgntsty_twitter . I store an instance of the class object as a variable local to the middleware function, such that it goes out of scope and gets destroyed when the pointer to the function gets released:
    func assetsMiddleware() -> Middleware<MyAppState> {
      var assetsService: AssetsService?
    
      return { dispatch, getState in { next in { action in
        guard let assetsAction = action as? AssetsAction else {
          next(action)
          return
        }
    
        switch assetsAction {
        case .initialize(let config):
          assetsService = AssetsService(store: MyApp.instance!.store)
          assetsService!.initialize(config: config)
    
        case .startSession(let sessionConfig):
          assetsService!.startSession(sessionConfig: sessionConfig)
    
        case .endSession:
          assetsService!.endSession()
    
        default:
          break
        }
    
        next(action)
        }}}
    }
    Jonah Neugass
    @bgntsty_twitter
    We try to avoid singletons as it makes unit tests more difficult.
    Christian Tietze
    @DivineDominion
    Where do you initialize your Store instance? If it's assembled in your app module, you can inject stuff upon object creation
    donatimariano
    @donatimariano
    @bgntsty_twitter where is this configuration coming from? Shouldn't that region be part of the overall app state or one of its substates?
    Jonah Neugass
    @bgntsty_twitter
    We initialize the data store instance in a dependency container:
    container.register(.singleton) { (
                ssoManager: CKIAppState.SSOManager,
                webConfiguration: WKWebViewConfiguration,
                configuration: Configuration,
                appInformation: StaticAppInformation,
                appReducerContainer: AppReducerContainer) -> AppDataStore in
                CK.assert(created == false)
                created = true
    
                let cookieStorage = HTTPCookieStorage.shared
                let webDataStore = webConfiguration.websiteDataStore
                let userDefaults = UserDefaults.standard
                let authenticationState = AuthenticationState(ssoManager: ssoManager, cookieStorage: cookieStorage, webDataStore: webDataStore, userDefaults: userDefaults)
    
                let initialAppState =  AppState(authenticationState: authenticationState,
                                                pushNotificationState: PushNotificationState(),
                                                alertState: .none,
                                                biometricAuthenticationState: BiometricAuthenticationState(),
                                                appRatingState: AppRatingState(),
                                                nuDetectConfigState: NuDetectConfigState(),
                                                regionState: RegionState(),
                                                csrfState: .none,
                                                traceIDState: .none)
                return AppDataStore(reducer: appReducerContainer.reduce, state: initialAppState, middleware: [], automaticallySkipsRepeats: true)
            }.resolvingProperties { container, dataStore in
                let loggingMiddleware: LoggingMiddleware = try container.resolve()
                let trackingMiddleware: TrackingMiddleware = try container.resolve()
                let regionChangeMiddleware: RegionChangeMiddleware = try container.resolve()
    
                let enableEventLogging: Bool = AppConfigurationFeatureFlags.all.enableEventLogging.value
                let darwinMiddleware: DarwinMiddleware = try container.resolve()
    
                var middleware: [MiddlewareContainer] =  [regionChangeMiddleware, trackingMiddleware, darwinMiddleware]
    
                if enableEventLogging {
                    middleware.append(loggingMiddleware)
                }
    
                dataStore.addMiddleware(middleware)
            }
    Jonah Neugass
    @bgntsty_twitter
    @donatimariano Configuration could probably live in the app state. This is an inherited project and some of the Redux stuff was done incorrectly initially, in fact, the original developer used a simple custom built Redux solution so that they could integrate with ReactiveSwift. I ended up replacing that solution with ReSwift and added some simple functionality to add reactive capabilities.
    public class AppDataStore: Store<AppState> {
    
        public func addMiddleware(_ middleware: [MiddlewareContainer]) {
            let reversedMiddleware = middleware.reversed()
            dispatchFunction = reversedMiddleware.reduce({ [unowned self] action in
                self._defaultDispatch(action: action)
            }, { dfunk, middleware in
                let dispatch: (ReSwift.Action) -> Void = { [weak self] in self?.dispatch($0) }
                let getState = { [weak self] in self?.state }
                return middleware.process()(dispatch, getState)(dfunk)
            })
        }
    
        public override func dispatch(_ action: ReSwift.Action) {
            if !Thread.isMainThread {
                DispatchQueue.main.sync {
                    super.dispatch(action)
                }
            } else {
                super.dispatch(action)
            }
        }
    }
    
    public protocol CKStoreSubscriber: StoreSubscriber {
        associatedtype State: StateType, Equatable
        var stateProperty: MutableProperty<State> { get }
    }
    
    public extension CKStoreSubscriber {
        func newState(state: State) {
            stateProperty.value = state
        }
    }
    donatimariano
    @donatimariano
    @bgntsty_twitter thanks, that makes sense now. I think in my case it's a bit different since ultimately the result is I dispatch an action that will change the current state so I don't need to have a reference to any other object. I do have some calculations being performed, but these are just calculations. They do not alter the state of any other object. Some of this calculations are mere algorithms, but some of them may also include business rules. So this is where I have the feeling it's not quite right. Like are middlewares supposed to have business rules at all? For example, let's say an app has an user address and a delivery address so a middleware may contain logic that will intercept the SetUserAddress action and it will check if the delivery address is null and in such case it will dispatch SetDeliveryAddress action as a side effect. My question would be... is this logic ok to have on a middleware or even an accompanion class? should it live in the state reducer? The thing is that I do like how my state reducers look like. They are really dummies in the sense they just do one to one assignments, but on the other hand, I'm not sure I like how my middlewares start looking as I keep adding more and more code to them.
    Jonah Neugass
    @bgntsty_twitter
    @donatimariano I'm certainly not a Redux expert, so I'm not sure if all business logic should live in middleware. It seems to me that reducers would be a fine place for business logic as long as the only thing they are doing is generating new state. If this were otherwise, all reducers would essentially be trivial as all they could do is set their associated state. Anything that generates side effects should go in middleware. Maybe @DivineDominion could chime in and enlighten us?
    Dario
    @Diarrhio
    I'm new to Redux myself. I kind of like putting all business logic in middleware because there is no ambiguity of where to look for business logic because it's in one place and one place only. I'd love to hear other opinions though
    Jonah Neugass
    @bgntsty_twitter
    @Diarrhio We currently have a hybrid approach using ReSwift in addition to a VIPER architecture with business logic being split between Presenters, Middleware, Resolvers and in some cases in state objects themselves. I'm trying to fix this but finding enough time for tech debt issues is always difficult.
    Dario
    @Diarrhio
    @bgntsty_twitter This whole project I'm redoing in Redux is the result of years of technical debt piling up. Somehow I convinced the higher ups to let me burn our project to the ground and rewrite it. The old project was a collapsing house of cards
    Jonah Neugass
    @bgntsty_twitter
    @Diarrhio Yah, our app was like that when I got to the company 5 years ago.
    Christian Tietze
    @DivineDominion
    @bgntsty_twitter "business logic" is somewhat vague in this context, I think. I do have conditions in my reducers, and I think these express part of the rules. Maybe a constructed example is to require a pending network request before a success is accepted, or similar.
    Though that contrived example might not even be a good contrived example :D
    Dario
    @Diarrhio

    Question for the group... perhaps I am using ReSwift wrong, but I often find myself in the situation where a some class needs to subscribe to different substates. Currently it's not possible for that class to inherit from StoreSubscriber and then subscribe to multiple substates using the following:

        store.store.subscribe(self) {
          return $0.select { $0.someSubstate.someValue }
        }

    and then do so again, for a different substate, because it needs to override newState with the value of the type returned by select

    So, I have to create all of these ancillary classes which inherit from StoreSubscriber subscribe to the substate and then call some callback of entity that created the class instance to notify it of the sub-state change
    Which gets to be a huge pain. I got fed up with it and did the following. I had my toplevel app store inherit from StoreSubscriber and not just subscribe to the whole state (i.e. no select). I then added a subscribe method to the top level app store which takes in a select function and a callback to call when the sub-state changes. I needed a set of helper classes to do this:
    Dario
    @Diarrhio
    open class Subscriber: Equatable {
      public static func == (lhs: Subscriber, rhs: Subscriber) -> Bool {
        return lhs.id == rhs.id
      }
    
      init(store: MyAppStore, id: Int) {
        self.id = id
      }
    
      func onUpdateState(state: MyAppState) {
        preconditionFailure("This method must be overridden")
      }
    
      func clear() {
        store?.removeSubscriber(subscriber: self)
      }
    
      private weak var store: MyAppStore?
      private let id: Int
    }
    
    private class SubscriberConcrete<T: Equatable>: Subscriber {
      init(store: MyAppStore,
           id: Int,
           select: @escaping (MyAppState) -> T,
           skipFirst: Bool,
           onChange: @escaping (T, T) -> Void) {
        self.select = select
        self.onChange = onChange
    
        currentState = select(store.state)
        if !skipFirst {
          onChange(currentState, currentState)
        }
    
        super.init(store: store, id: id)
      }
    
      override func onUpdateState(state: MyAppState) {
        let nextState = select(state)
        if nextState != currentState {
          // onChange call may change the state again making an infinite loop. So we change currentState first.
          let currentStateCopy = currentState
          currentState = nextState
          onChange(nextState, currentStateCopy)
        }
      }
    
      private let select: (MyAppState) -> T
      private let onChange: (T, T) -> Void
      private var currentState: T
    }
    I added a generic subscribe method to my top level store:
      func subscribe<T: Equatable>(select: @escaping (LunarEmbeddedState) -> T,
                                   skipFirst: Bool,
                                   onChange: @escaping (T, T) -> Void) -> Subscriber {
    
        let subscriber = SubscriberConcrete(store: self,
                                            id: count,
                                            select: select,
                                            skipFirst: skipFirst,
                                            onChange: onChange)
        subscribers.append(subscriber)
    
        return subscriber
      }
    And then finally in the newState, I just iterate through all of the subscribers and call onUpdateState on them:
      func newState(state: LunarEmbeddedState) {
        for subscriber in subscribers {
          subscriber.onUpdateState(state: state)
        }
      }
    I just whipped all this code up in an hour, so it's obviously got issues, but it feels like this type of functionality should be built into the ReSwift store itself as this is way more convenient. But I'm wondering if I'm missing something
    Jonah Neugass
    @bgntsty_twitter
    @Diarrhio You can subscribe to a composite of substates...here's what the ReSwift docs say:
    Create a subscription of several substates combined
    Just create a struct representing the data model needed in the subscriber class, with a constructor that takes the whole app state as a param. Consider this constructor as a mapper/selector from the app state to the subscriber state. Being MySubState a struct and conforming to Equatable, ReSwift (by default) will not notify the subscriber if the computed output hasn't changed. Also, Swift will be able to infer the type of the subscription.
    
    struct MySubState: Equatable {
        // Combined substate derived from the app state.
    
        init(state: AppState) {
            // Compute here the substate needed.
        }
    }
    store.subscribe(self) { $0.select(MySubState.init) }
    
    func newState(state: MySubState) {
        // Profit!
    }
    Dario
    @Diarrhio
    @bgntsty_twitter thanks for that. I guess I missed that. Still, and this just my taste here, I don't like creating a bunch of ancillary classes for things like that since it kinda puts me in a similar situation I was mentioning and I feel it tends to clutter up the codebase. I kinda like just being able to do something like:
    store!.subscribe(
          select: { $0.appModel },
          skipFirst: false,
          onChange: { new, _ in
            self.downloadWatermark(watermarkUrl: new?.watermarkUrl)
        })
    and then be done with it
    Jonah Neugass
    @bgntsty_twitter
    Makes sense if that works for you. I find the combined state to be useful as the skipRepeats works on the combined state as whole, where as I don't think that's the case in your solution.
    Dario
    @Diarrhio
    That's correct, you will have to subscribe multiple times, with each element you are interested in. Guess it's just a matter of taste :)
    Anyways, thanks for your help
    Jonah Neugass
    @bgntsty_twitter
    Sure thing!
    Malcolm Jarvis
    @mjarvis

    @Diarrhio I've used a BlockSubscriber type to do this sort of thing.
    Similar to https://github.com/ReSwift/ReSwift/blob/d39aaf108c987fe7659a5fafe39019bd30c8c3fb/ReSwiftTests/TestFakes.swift#L172

    Then instead of subscribing self, you subscribe the block subscriber, and maintain a dispose bag of those blocksubscribers for discarding.

    This sort of pattern is fairly similar to adding a reactive extension to ReSwift.

    Dario
    @Diarrhio
    @mjarvis thanks, that's kinda what I was doing, but not generically like that. Seems like a good approach. One thing I like about my approach above is that it gives you the old value too :)
    Christian Tietze
    @DivineDominion
    @bgntsty_twitter I like the way your block-based subscription reads! In terms of "ancillary classes", I wonder if end up with more objects on the head this way because you need to encapsulate every block-based subscription in an object; and also I wonder why you'd write 1 subscriber to react to multiple events. Might there be hidden potential for splitting things up or reorganizing the code a bit more? I could see a subscriber protocol that offers a static func substate<T>(appState: AppState) -> T factory that produces the selected substates for you without much boilerplate. Could be a tuple, really.
    Christian Tietze
    @DivineDominion
    I am working on a demo project that uses ReSwift to re-implement Elm Mario (https://github.com/avh4/elm-mario/ playable in browser). I started with a couple of actions to affect the flow and state of animations, and Middleware to know when to dispatch the changes. But I found that it works really, really nice when you put that into a single reducer that contains the game loop, if you will.
    with that, the code began to look more like the Elm original, too
    The Middleware for movement as a side effect of the tick event was used in this commit, for example: https://github.com/CleanCocoa/ReSwift-Mario/blob/b7fd7d4c1518de3ef66d0123067ddeecf2aae995/State/MovementMiddleware.swift -- you can browse the "State" module at that commit to see the others like the Middlewares for jumping https://github.com/CleanCocoa/ReSwift-Mario/blob/b7fd7d4c1518de3ef66d0123067ddeecf2aae995/State/JumpingMiddleware.swift
    if you have any thoughts on the approach so far, I'd be happy to hear them! :)
    nanocba
    @nanocba
    Hello everyone! When working within a middleware most of the time I need to write something like this guard let state = getState() else { ... } has anyone come up with any pattern that would make middlewares work with a valid i.e. non-null state? I was thinking about wrapping the middleware into a container and the container would have this logic so I don't have to write it every single time. Any other ideas?
    Malcolm Jarvis
    @mjarvis
    @nanocba that sounds like a good way to do it. For reference, the reasons it returns an optional is because before the ReSwiftInit action occurs, state can be nil, and also because it is possible that the Store gets discarded while some async middleware is operating (Though this is rare as most people have a global store)