Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@connect discussion and implementation #400

Open
Dimillian opened this issue Mar 25, 2019 · 8 comments
Open

@connect discussion and implementation #400

Dimillian opened this issue Mar 25, 2019 · 8 comments

Comments

@Dimillian
Copy link

Dimillian commented Mar 25, 2019

So finally decided to play around an implementation of something approaching @connect decorator from React/Redux. Was initially made for ReKotlin by our Android team, but works the same with ReSwift.
It's mostly syntax sugar, but our end goal is to make the getter on store.state unavailable, so no more free access to the global store, it should all be in your component local Props struct.

The StoreConnector implementation

class StoreConnector<Props: Equatable>: StoreSubscriber {
    
    private var props: Props? = nil
    private let mapStateToProps: (AppState) -> Props?
    private let render: (Props) -> Void
    
    init(mapStateToProps: @escaping (AppState) -> Props?,
         render: @escaping (Props) -> Void) {
        self.mapStateToProps = mapStateToProps
        self.render = render

        store.subscribeQueue(self) {
            $0.skipRepeats({ [weak self] (oldState: AppState, newState: AppState) -> Bool in
                self?.props == self?.mapStateToProps(newState)
            })
        }
    }
    
    internal func newState(state: AppState) {
        props = mapStateToProps(state)
        guard let props = props else {
            return
        }
        DispatchQueue.main.async {
            self.render(props)
        }
    }
    
    func unsubscribe() {
        store.unsubscribe(self)
    }
    
    deinit {
        store.unsubscribe(self)
    }
    
}

And now some example usage

class SomeComponent {
    private struct Props: Equatable {
        let items: [ObjectId: FeedItem]
        let datasource: [ObjectId]

    }
    
    private var props: Props? {
        didSet {
           // Actually do you refresh/render
        }
    }
    private var storeConnector: StoreConnector<Props>?

    func viewDidLoad() {
        storeConnector = StoreConnector<Props>(mapStateToProps: { (state) -> Props? in
            return Props(items: state.feedState.items,
                        datasource: state.feedState.feeds[id] ?? [])
            }, render: { [weak self](props) in
                self?.props = props
        })
    }
}

What do you guys think?

@DivineDominion
Copy link
Contributor

DivineDominion commented Mar 25, 2019

This looks interesting, but I think I need more background info :)

I'm not fluent in the JS origins of ReSwift and don't know what the @connect operator does. Can you quickly sketch the objective of this so it's easier for Swift devs like me to follow along?

Implementation wise, the StoreConnector is basically a subscriber that asynchronously dispatches a callback when it's done with its job of mapping state to props (where "props" are view-related, it seems). Why async? (Ok, why not?)

When you have to specify the mapping in viewDidLoad, how is this different from replacing the StoreConnector initialization with a subscription like this:

        store.subscribe(self) {
            $0.skipRepeats({ [weak self] (oldState: AppState, newState: AppState) -> Bool in
                SomeComponent.Props(items: newState.feedState.items,
                        datasource: newState.feedState.feeds[id] ?? [])
            })
        }

And then make the SomeComponent itself the subscriber?


Disclaimer: I don't subscribe views or view controllers in any of my apps. I use Presenters for this. My presenters do the state-to-view-model-mapping; your suggestion is shorter, but then I'd have to write the conversion code from state to Props in the view controller again. Or I leave all that inside the presenter, but then the presenter doesn't do much itself. Is your pattern useful for quickly subscribing lots of light-weight components? Does it work well/better/... for table view cells?

@ngeplurk
Copy link

I'm using ReRxSwift in my latest project, the bindings layer for RxSwift and ReSwift, similar to React Redux for React and Redux.

@jimisaacs
Copy link

jimisaacs commented Mar 27, 2019

@Dimillian the code you shared is a bit uncanny to code that we've put together across ReSwift + ReKotlin. There's some differences, but I think we try to should share a bit more.
Key similarities:

  • component based architecture
  • transforming app state to component state
  • render logic

Overall I think we've taken a much more strict approach in that even a component does not have access to the store, and is managed by, you guessed it, a componentManager.

View controller / fragment owns componentManager, componentManager manages component state subscriptions. This way, on viewWillAppear, you call subscribe on the component manager, and unsubscribe in the appropriate other life-cycle. Overall, it's not the component's jobs to manage their subscription life-cycles.

This was an approach that felt like it would allow a better separation of views from screens, but it does not mean I'm attached to it.

@jimisaacs
Copy link

jimisaacs commented Mar 27, 2019

@Dimillian I should also mention an advantage of removing the store from components is that they become completely autonomous units of code, that can be tested as such.

@Dimillian
Copy link
Author

@jimisaacs Uncanny indeed, I took a look at ReRxSwift and it's very close to what I'm trying to do!
And yes, @DivineDominion, the goal of this pattern is to provide a bit more "rails" for component so they can be totally independant from store and appstate, and much more testable outside of the app.

The downside is that you need a bit more code, and your props struct tend to be a bit heavier because some big components sometime need to copy a pretty big part of the AppState.

@DivineDominion
Copy link
Contributor

The downside is that you need a bit more code, and your props struct tend to be a bit heavier because some big components sometime need to copy a pretty big part of the AppState.

I don't see a problem with that. Maybe because I do this in my app all the time -- I almost never use objects from the ReSwift-powered State module directly and wrap or convert things all the time to isolate the modules.

If the basic mechanism is indeed to wrap this:

        store.subscribe(self) {
            $0.skipRepeats({ [weak self] (oldState: AppState, newState: AppState) -> Bool in
                SomeComponent.Props(items: newState.feedState.items,
                        datasource: newState.feedState.feeds[id] ?? [])
            })
        }

then I find the result a bit too verbose for my taste, with all the parens and brackets and nested closures:

        storeConnector = StoreConnector<Props>(mapStateToProps: { (state) -> Props? in
            return Props(items: state.feedState.items,
                        datasource: state.feedState.feeds[id] ?? [])
            }, render: { [weak self](props) in
                self?.props = props
        })

Conceptually, there the mapStateToProps, and then there's the Props creation with the render callback. It's rather simple, but it doesn't look like it's simple. Ideally, I'd like to get rid of the render part so one ends up with the object conversion only.

A half-assed attempt to simplify could be to just split things up and un-nest them :)

        storeConnector = StoreConnector<Props>()
            .mapStateToProps: { (state) -> Props? in
                return Props(items: state.feedState.items,
                            datasource: state.feedState.feeds[id] ?? [])
            }.render { { [weak self] (props) in
                self?.props = props
        }

Then again, maybe I'm being horrible here and don't see how Rx influenced my taste for the worse.

@Dimillian
Copy link
Author

Again, it's mostly sugar, I like your approach, it was at some point my V0.
But when making visual component, it's nice to have a clear function render provided by some contract or inheritance, with clear oldProps? and actual new props. (I know I could do all the render in the didSet with oldValue), but again, less appealing for UI component.

I'm converting most of my app to this new pattern, and having internal props clearly mapped/calculated and isolated from the store/state is really a big plus in term of code readability, testing and actual render pass.

Actual base class could look like this

class ConnectedViewController<Props: Equatable>: UIViewController {
    
    private var connector: StoreConnector<Props>!
    
    private(set) var props: Props? {
        didSet {
            guard let props = props else {
                return
            }
            render(oldProps: oldValue, props: props)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        connector = StoreConnector<Props>(mapStateToProps: { [weak self] (state) -> Props? in
            return self?.mapStateToProps(state: state)
            }, render: { [weak self](props) in
                self?.props = props
        })
        
    }
    
    deinit {
        connector.unsubscribe()
    }
    
    func mapStateToProps(state: AppState) -> Props {
        fatalError("Need to be implemented by subclass")
    }
    
    func render(oldProps: Props?, props: Props) {
        fatalError("Need to be implemented by subclass")
    }
}

@lumiasaki
Copy link
Contributor

lumiasaki commented May 13, 2021

Browsing issues and found this interesting one. As an iOS developer who worked on React Native with Redux few years ago, found the differences of concepts just like connect, state and props are super interesting.

In my understanding, essentially, connect is a currying function in JavaScript version of Redux that maps state and dispatches them to the props of a Component. It first takes two function-type arguments, mapStateToProps and mapDispatchToProps, and then takes a component instance as another argument. Finally, a Component instance is returned.

The mapStateToProps function is responsible for handling the mapping relationship from state to props, where state is the state obtained from the global Store, and props is the Component relative to the states through the connect. In this way, the Component will only take the data it needs from the Store as state mapping as props, effectively controlling complexity and improving robustness. In addition, the children of a Component no longer need to pay attention to the concept of Store or unnecessary part of states, and all the data needed is passed in by the parent component via connect function magically.

I am not sure introducing this concept into ReSwift is a great idea but obviously these two libraries in slightly different ways.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants