Published 4 min read

Puredux 1.1 - Release Notes

Puredux v1.1 is finally out. Puredux is a UDF architecture framework in Swift for app state management with Redux/Flux ideas in mind.
Puredux 1.1 - Release Notes
Photo by Joshua Reddekopp / Unsplash

It's been a long time since the first release of Puredux.

Finally, I've rolled out an update. Changes affected both PureduxStore and PureduxSwiftUI bindings. PureduxUIKit got a minor update with the support of the new features.

TBH, there were enough changes for a 2.0 release, but we are humble.

New Puredux Store Factory API

Old API was a bit misleading. In the old version, RootStore was called a Store but acted like a factory.

I fixed this weird confusion, and now things are named what they are.

Before:

let rootStore = RootStore<AppState, Action>(
    queue: StoreQueue = .global(qos: .userInteractive)
    initialState: initialState, 
    reducer: reducer
)

let store: Store<AppState, Action> = rootStore.store()

Now:

let storeFactory = StoreFactory<AppState, Action>(
    initialState: initialState, 
    qos: .userInteractive,
    reducer: reducer
)

let store: Store<AppState, Action> = storeFactory.rootStore()

Another important change is that StoreFactory doesn't allow custom queues anymore.
There were important changes made under the hood that made it incompatible with the main queue.

Now it's also less error-prone because there is no way to misconfigure it with a wrong queue.

Actions Interceptor which is used for side effects is defined a bit differently:

Before:

rootStore.interceptActions { action in
    guard let action = ($0 as? AsyncAppAction) else  {
        return
    }
	//do some long-running async work here
    let resultAction = ...
    
    DispatchQueue.main.async {
        store.dispatch(resultAction)
    }   
}

Now:

let storeFactory = StoreFactory<AppState, Action>(
    initialState: initialState, 
    interceptor:  { action, dispatch in
        guard let action = ($0 as? AsyncAppAction) else  {
            return
        }
	    //do some long-running async work here
	    let resultAction = ...
	    
        DispatchQueue.main.async {
            dispatch(resultAction)
        } 
    },
    reducer: reducer
)

New store types

The release brings new store types to the table.

ScopeStore

Scope store is a rebranding of a good old proxy store. I think it's a better naming that reflects the essence and the purpose. Behaviour is exactly the same.

Scope store creation is almost the same as it used to be

Before:

let storeProxy = rootStore.store().proxy { appState in appState.subState }

Now:

let scopeStore = storeFactory.scopeStore { appState in appState.subState }

ChildStore

ChildStore is a bit of a ground braking thing for Puredux because it shifts its initial redux-like single store paradigm and allows to combine it with pluggable multi store architecture.

On the one hand, child store is a separate store with it's own initial local state and reducer.

On another hand, child store creates child-parent hierarchy and gets attached to the root store.

It can receive root store's updates, dispatch actions to the root store and observe the state as a composition of parent store's state and its local states.


let factory = StoreFactory<SharedState, Action>(
    initialState: SharedState(), 
    reducer: { ... }
)

var storeObjectA: StoreObject<(SharedState, StateA), Action> = factory.childStore(
	initialState: StateA(),
    reducer:  { ... }														
)

var storeObjectB: StoreObject<(SharedState, StateB), Action> = factory.childStore(
	initialState: StateB(),
    reducer:  { ... }														
)

var storeA: Store<(SharedState, StateA), Action> = storeObjectA.store()
var storeB: Store<(SharedState, StateB), Action> = storeObjectB.store()


Child store has its own state lifecycle. The child store state's lifecycle is controlled by the lifecycle of the store aka. StoreObject.
The state is created and released together with the store object.

Child store provides actions isolation. Actions dispatched to one child store are also dispatched to the root store, but never to another child store. It's also true for the actions dispatched from the async actions interceptor.

These two features solve the problems described in Common Redux Pitfalls on iOS: Deep Navigation Stack.

When you have a deep navigation stack you

  • don't want actions to be dispatched to all screen reducers. Especially if you don't want to hassle with Async actions.
  • don't want to manually create/release screen states.

That's what the child store handles for you.

PureduxStore Documentation

It's a minor release so the old API is still there and available. However, it's marked as deprecated to produce warnings in the places that require updates.

Old API is likely to be removed in the next major release. More details as well as a migration guide can be found in docs

New PureduxSwiftUI API

PureduxSwiftUI received a bit more elegant API and got support for the new PureduxStore features.

If we have a plain dummy view without any state lifecycle:

struct FancyView: View {
    let title: String
    let didAppear: () -> Void
    
    var body: some View {
        Text(title)
            .onAppear { didAppear() }
    }
}

We can connect to the root store as easy as that:

 
let storeFactory = StoreFactory<AppState, Action>(
    initialState: AppState(),
    reducer: { state, action in state.reduce(action) }
)
 
let envStoreFactory = EnvStoreFactory(storeFactory: storeFactory)
 
UIHostingController(
    rootView: ViewWithStoreFactory(envStoreFactory) {
        
        ViewWithStore { state, dispatch in
            FancyView(
              title: state.title,
              didAppear: { dispatch(FancyViewDidAppearAction()) }
            )
        }
    }
)

Switching to a ScopeStore would be as simple as that:


UIHostingController(
    rootView: ViewWithStoreFactory(envStoreFactory) {
        
        ViewWithStore { title, dispatch in
            FancyView(
              title: title,
              didAppear: { dispatch(FancyViewDidAppearAction()) }
            )
        }
        .scopeStore({ $0.title })
    }
)
        

If we need to switch to a ChildStore with a state lifecycle attached to the View's lifecycle, we can do this:


UIHostingController(
    rootView: ViewWithStoreFactory(envStoreFactory) {
        
        ViewWithStore { state, dispatch in
            FancyView(
              title: state.childState,
              dispatch: dispatch
            )
        }
        .childStore(
            initialState: "Fancy view title",
            stateMapping: { appState, childState in
                (appState: appState, childState: childState)
            },
            reducer: { ... }
        )
    }
)

We can deduplicate state changes by doing this:


UIHostingController(
    rootView: ViewWithStoreFactory(envStoreFactory) {
        
        ViewWithStore { state, dispatch in
            FancyView(
              title: state.title,
              didAppear: { dispatch(FancyViewDidAppearAction()) }
            )
        } 
        .removeStateDuplicates(.equal { $0.title })
    }
)


We still can introduce an additional presentation layer for the view and move its evaluation to a separate queue:


ViewWithStore(
  props: { state, dispatch in Props(...) }, 
  content: { props in FancyView(props: props) }
)
.usePresentationQueue(.serialQueue(queue))


PureduxSwiftUI Documentation

Old API is available and marked as deprecated to produce warnings in the places that require updates. More details as well as a migration guide can be found here

PureduxUIKit - Child Store support

PureduxUIKit got support for the Child store:


let factory = StoreFactory<SharedState, Action>(
    initialState: SharedState(), 
    reducer: { ... }
)

var childStore: StoreObject<(SharedState, StateA), Action> = factory.childStore(
	initialState: StateA(),
    reducer:  { ... }														
)

let viewController = FancyViewController()
viewController.with(
	store: childStore,
	props: { state, store in Props(...) }
)

present(viewController, animated: true)

PureduxUIKit Documentation

PureduxUIKit Documentation can be found here