3 min read

Puredux 2.0 Release. The Massive One.

Puredux 2.0 Release. The Massive One.

Related: Puredux


Recently I released a massive update for the Puredux.

The most recent cool features:

  • Single/Multiple Store tree architecture
  • UIKit/SwiftUI API improvements (known as Puredux Sugar)
  • Dependency Injection
  • Performance tuning features
  • State-driven side effects
  • An Insane Documentation update

Single & Multiple Store Tree Architecture

There were attempts to build multi-store configurations in Puredux but in older versions it was troublesome:

  • There were caveats related to async actions results dispatching to the right store. It was actually solved but with ugly workarounds inside which I hated.

  • There was also a thread explosion issue that could occur in deeply nested trees hierarchy because each child store was operating on its own queue. That's why in older versions the hierarchy depth was limited by 2 and supported only the "root parent" + "child" configuration.

I've rewritten the internals to allow the whole store hierarchy to operate on a single queue which allowed me to improve the performance dramatically.

Now a single action dispatch is always a single hop to the internal queue and a single update of each observer, no matter the tree depth.

It also means that multistore store decomposition is now a zero cost which is kinda of cool because it solves all questions related to scope and feature isolation, as well as state lifecycle.

UIKit & SwiftUI integration API improvements

UIKit API was slightly tuned to be as close to UIViewController and UIView as possible:


final class MyViewController: ViewController {
    let store = StoreOf[\.root]
    
    override func viewDidLoad() {
        super.viewDidLoad()
         
        subscribe(store) { [weak self] in
            // Handle updates with the derived props
            self?.updateUI(with: $0)
        }
     }
     
     private func updateUI(with state: AppState) {
         // Update UI elements with the new view state
     }
}

SwiftUI API was massively refined for simplicity and more of a SwiftUI vibes:

struct ContentView: View  {
    @State var store = StoreOf[\.root]
    
    @State var viewState: (AppState)?
    
    var body: some View {
        MyView(viewState)
            .subscribe(store) { viewState = $0 }
            .onAppear {
                dispatch(SomeAction())
            }
    }
}

Dependency Injection

In practice, it turns out that it's hard to make the code 100% free from dependencies even if it is a pure reducer. There definitely will be something that shows up. A certain uuid or exact moment of time, environment config, or maybe a feature flag.

Since the Puredux stores operate in the background there are certain caveats related to using Dependency Injection in a multithreaded environment.

So I made it.

Performance Tuning

I added advanced performance tuning capabilities. The idea is the following:

  • You are casually building an iOS app with Puredux
  • One of the screens in your app turns out to be insanely complicated and performs poorly. Every app should have a complicated screen, isn't it?
  • You spot the reason for the poor performance and apply one of the built-in Puredux performance tuning solutions.
  • It starts to work well.

Puredux now supports the following toolset:

  • Granular UI updates: any View or UIViewController or UIView can be individually subscribed to the store or a part of it.
  • UI updates with deduplication and debouncing: a silver bullet against inevitably frequent store updates
  • UI updates breakdown: UI updates can be split into 2 steps, allowing to offload heavy computations to a separate background queue.

State Driven Side Effects

I've been prototyping the feature for a very long time and could never get it done without a ton of boilerplate code. Finally, I did.

Here is a more detailed explanation of the idea: State-Driven Side Effects

There is actually a challenge in side effects implementation for UDF architectures. You need to put side effects somewhere and each place has its trade-offs.

  1. Intercepting middleware makes life with a multistore configuration harder: actions async should be executed once and the result delivered to the right store.
  2. Async actions bring dependencies to actions which makes it hard to keep actions pure plain structs
  3. Feedback loops require execution middleware or bring dependencies into reducers.

State-driven side effects do not have these downsides.

  • They don't require middleware, allowing for complicated multistore hierarchies
  • Actions can remain plain structs without dependencies.
  • Reducers can remain pure functions without dependencies.

State-driven side effects make complicated sequences of different kinds of work simple. As well as task cancellation, delaying, and waiting or retries.

Here is an example:


// Add effect state to the state
struct AppState {
   private(set) var theJob: Effect.State = .idle()
}

// Add related actions**

enum Action {
   case jobSuccess(Something)
   case startJob
   case cancelJob
   case jobFailure(Error)
}

// Handle actions in the reducer
 
extension AppState {
   mutating func reduce(_ action: Action) {
       switch action {
       case .jobSuccess:
           theJob.succeed()
       case .startJob:
           theJob.run()
       case .cancelJob:
           theJob.cancel()
       case .jobFailure(let error):
           theJob.retryOrFailWith(error)
       }
   }
}

// Add SideEffect to the store:
 
let store = StateStore<AppState, Action>(AppState()) { state, action in 
   state.reduce(action) 
}
.effect(\.theJob, on: .main) { appState, dispatch in 
    // effect will be created and scheduled for execution 
    // once `theJob` state turns into "running" state			  
   Effect {
       do {
           let result = try await apiService.fetch()
           dispatch(.jobSuccess(result))
       } catch {
           dispatch(.jobFailure(error))
       }
   }
}

// Dispatch action to run the job:

store.dispatch(.startJob)

Documentation Update

That's probably the best documentation I ever wrote in my life.

You need to LOOK AT THIS