DEV Community

Cover image for Redux-like state container in SwiftUI. Side effects.
Sergey Leschev
Sergey Leschev

Posted on • Updated on

Redux-like state container in SwiftUI. Side effects.

We already implemented a unidirectional flow that accepts user actions and modifies the state, but what about async actions which we usually call side effects. How to bake a support for async tasks into our store type? I think it is a good time to introduce the usage of Combine framework that perfectly fits async task processing.

typealias Reducer<State, Action, Environment> =
    (inout State, Action, Environment) -> AnyPublisher<Action, Never>?
Enter fullscreen mode Exit fullscreen mode

We add support for async tasks by changing Reducer typealias, it has the additional parameter called Environment. Environment might be a plain struct that holds all needed dependencies like service and manager classes.

func appReducer(
    state: inout AppState,
    action: AppAction,
    environment: Environment
) -> AnyPublisher<AppAction, Never>? {
    switch action {
    case let .setSearchResults(repos):
        state.searchResult = repos
    case let .search(query):
        return environment.service
            .searchPublisher(matching: query)
            .replaceError(with: [])
            .map { AppAction.setSearchResults(repos: $0) }
            .eraseToAnyPublisher()
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Side Effect is a sequence of Actions which we can publish using Combine framework’s Publisher type. It allows us to handle async job and then publish an action that will be used by reducer to change the current state.

final class Store<State, Action, Environment>: ObservableObject {
    @Published private(set) var state: State

    private let environment: Environment
    private let reducer: Reducer<State, Action, Environment>
    private var cancellables: Set<AnyCancellable> = []

    init(
        initialState: State,
        reducer: @escaping Reducer<State, Action, Environment>,
        environment: Environment
    ) {
        self.state = initialState
        self.reducer = reducer
        self.environment = environment
    }

    func send(_ action: Action) {
        guard let effect = reducer(&state, action, environment) else {
            return
        }

        effect
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: send)
            .store(in: &cancellables)
    }
}
Enter fullscreen mode Exit fullscreen mode

We build a Store type that supports async tasks. Usually, reducer resolves an action by applying it on top of the state. In case of an async action, reducer returns it as Combine Publisher, then Store run it and send result back to the reducer as a plain action.



Contacts
I have a clear focus on time-to-market and don't prioritize technical debt. And I took part in the Pre-Sale/RFX activity as a System Architect, assessment efforts for Mobile (iOS-Swift, Android-Kotlin), Frontend (React-TypeScript) and Backend (NodeJS-.NET-PHP-Kafka-SQL-NoSQL). And I also formed the work of Pre-Sale as a CTO from Opportunity to Proposal via knowledge transfer to Successful Delivery.

πŸ›©οΈ #startups #management #cto #swift #typescript #database
πŸ“§ Email: sergey.leschev@gmail.com
πŸ‘‹ LinkedIn: https://linkedin.com/in/sergeyleschev/
πŸ‘‹ LeetCode: https://leetcode.com/sergeyleschev/
πŸ‘‹ Twitter: https://twitter.com/sergeyleschev
πŸ‘‹ Github: https://github.com/sergeyleschev
🌎 Website: https://sergeyleschev.github.io
🌎 Reddit: https://reddit.com/user/sergeyleschev
🌎 Quora: https://quora.com/sergey-leschev
🌎 Medium: https://medium.com/@sergeyleschev

Top comments (2)

Collapse
 
stsmall profile image
Stanislav Shiyanovskiy

Thank you for the article. Can you explain how to return a few actions. For example, my system got a message from the server. I need to save it in the store, updated DB and make something else. It depends I should return 3 actions from the reducer in your article (in my projects I use middlewares). How do you solve this problem in your projects?

Collapse
 
sergeyleschev profile image
Sergey Leschev

Thank you for your question.
Normalization and composition keep our app state simple and maintainable.

State normalization.
State composition.
Reducer composition.
Derived stores.

dev.to/sergeyleschev/redux-like-st...