Yet another UDF state management lib
- Minimal API
- Single/Multiple Stores
- Operates on background queue
- Thread safe stores
- Simple actions interceptor for side effects
This repo has been moved to Puredux monorepo. Follow the installation guide there.
If you're looking to contribute or raise an issue, head over to the main repository where it's being developed now.
PureduxStore is available as a part of Puredux via Swift Package Manager. To install it, in Xcode 11.0 or later select File > Swift Packages > Add Package Dependency... and add Puredux repositoies URLs for the modules requried:
https://github.com/KazaiMazai/Puredux
- State is a type describing the whole application state or a part of it
- Actions describe events that may happen in the system and mutate the state
- Reducer is a function, that describes how Actions mutate the state
- Store is the heart of the whole thing. It takes Initial State and Reducer, performs mutations when Actions dispatched and deilvers new State to Observers
- Import:
import PureduxStore
- Create initial app state:
let initialState = AppState()
and Action
protocol:
protocol Action {
}
- Create reducer:
let reducer: (inout AppState, Action) -> Void = { state, action in
//mutate state here
}
- Create root store with initial app state and reducer. Get light-weight store:
let factory = StoreFactory<AppState, Action>(
initialState: initialState,
reducer: reducer
)
let store = factory.rootStore()
- Setup actions interceptor for side effects:
Let's have an AsyncAction
protocol defined as:
protocol AsyncAction: Action {
func execute(completeHandler: @escaping (Action) -> Void)
}
and some long running action that with injected service:
struct SomeAsyncAction: AsyncAction {
@DI var service: SomeInjectedService
func execute(completeHandler: @escaping (Action) -> Void) {
service.doTheWork {
switch $0 {
case .success(let result):
completeHandler(SomeResultAction(result))
case .success(let error):
completeHandler(SomeErrorAction(error))
}
}
}
}
Execute side effects in the interceptor:
let storeFactory = StoreFactory<AppState, Action>(
initialState: initialState,
interceptor: { action, dispatch in
guard let action = ($0 as? AsyncAppAction) else {
return
}
DispatchQueue.main.async {
action.execute { dispatch($0) }
}
},
reducer: reducer
)
- Create scoped stores with substate:
let scopedStore: Store<SubState, Action> = storeFactory.scopeStore { appState in appState.subState }
- Create store observer and subscribe to store:
let observer = Observer<SubState> { substate, completeHandler in
// Handle your latest state here and dispatch some actions to the store
scopedStore.dispatch(SomeAction())
guard wannaKeepReceivingUpdates else {
completeHandler(.dead)
return
}
completeHandler(.active)
}
scopedStore.subscribe(observer: observer)
- Create child stores with local state and reducer:
let childStoreObject: StoreObject<(AppState, LocalState), Action> = storeFactory.childStore(
initialState: LocalState(),
reducer: { localState, action in
localState.reduce(action: action)
}
)
let childStore = childStoreObject.store()
let observer = Observer<(AppState, LocalState)> { stateComposition, complete in
// Handle your latest state here and dispatch some actions to the store
childStore.dispatch(SomeAction())
guard wannaKeepReceivingUpdates else {
completeHandler(.dead)
return
}
completeHandler(.active)
}
childStore.subscribe(observer: observer)
Old API will be deprecated in the next major release. Please consider migration to the new API.
Click for details, it's not a big deal
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()
MainQueue is not available for Stores any more. Since now, stores always operate on a global serial queue with configurable QoS.
Before:
rootStore.interceptActions { action in
guard let action = ($0 as? AsyncAppAction) else {
return
}
DispatchQueue.main.async {
action.execute { store.dispatch($0) }
}
}
Now:
let storeFactory = StoreFactory<AppState, Action>(
initialState: initialState,
interceptor: { action, dispatched in
guard let action = ($0 as? AsyncAppAction) else {
return
}
DispatchQueue.main.async {
action.execute { dispatch($0) }
}
},
reducer: reducer
)
Before:
let storeProxy = rootStore.store().proxy { appState in appState.subState }
Now:
let scopeStore = storeFactory.scopeStore { appState in appState.subState }
Click for details
- It can be done with the help of PureduxUIKit or PureduxSwiftUI
StoreFactory is a factory for Stores and StoreObjects. It suppports creation of the following store types:
- Root Store - plain Store as proxy to the factory's' root store
- Scope Store - scoped Store as proxy to the factory root store
- Child Store - child StoreObject with
(Root, Local) -> Composition
state mapping and it's own lifecycle
- By default, it works on a global serial queue with
userInteractive
quality of service. QoS can be changed.
- Store is a lightweight store.
- It's a proxy to its parent store: it forwards subscribtions and all dispatched Actions to it.
- Store is designed to be passed all over the app safely without extra effort.
- It's threadsafe. It allows to dispatch actions and subscribe from any thread.
- It keeps weak reference to the parent store, that allows to avoid creating reference cycles accidentally.
- StoreObject is a class store.
- It's a proxy to its parent store: it forwards subscribtions and all dispatched Actions to it.
- It's designed to manage stores lifecycle
- It's threadsafe. It allows to dispatch actions and subscribe from any thread.
- It keeps a strong reference to the root store, that's why requries a careful treatment.
Call store observer's complete handler with dead status:
let observer = Observer<State> { state, completeHandler in
//
completeHandler(.dead)
}
It does both. Puredux allows to have a single store that can be scoped to proxy substores for sake of features isolation. It also allows to have a single root store with multiple plugable child stores for even deeper features isolation.
Puredux is designed in a way that you can seamlessly scale up to multiple stores when needed.
- Start with a single store.
- If the single app state is starting to feel overbloated think about using scope stores.
- If you start to plug/unplug some parts of the single state it might be a sign to start using child stores.
Click for details
- StoreFactory has an internal root store object,
- Root Store is Store that is created by
rootStore()
method - Root Store is a simple proxy to the factory's root store object
- Root Store keeps weak reference to the factory root store object store
Root store object lifecycle and its state managed by factory. Initalized together with the factory and released when factory is released It exist as long as the factory exist.
let store = factory.rootStore()
No. State is initialized when factory is created. Then it lives together with it. Typically factory's' lifecycle matches app's lifecycle.
Click for details
- Scoped store is a proxy to the root store
- Scoped doesn't have its own local state.
- Scoped doesn't have its own reducer
- Scoped store's state is a mapping of the root state.
- Doesn't create any child-parent hierarchy
let scopedStore: Store<Substate, Action> = storeFactory.scopeStore { appState in appState.subState }
- No, Proxy Store observers are triggered at every root store state change.
- The purpose is to scope entire app's state to local app feature state
Click for details
- Child store is a separate store
- Child store has its own local state
- Child store has its own local state reducer
- Child store is attached to the factory root store
- Child store's state is a composition of parent state and local state
- Creates child-parent hierarchy
StoreFactory allows to create child stores. You should only provide initial state and reducer:
let childStoreObject: StoreObject<(AppState, LocalState), Action> storeFactory.childStore(
initialState: LocalState(),
reducer: { localState, action in
localState.reduce(action: action)
}
)
Child store is a StoreObject
it will exist while you keep a strong reference to it.
Why root store and scope store is a Store<State, Action> and child store is a StoreObject<State, Action>
?
Root store's and scope store's lifecycle is controlled by StoreFactory. They exist while factory exist. Typically during the whole app lifecycle.
Child store is for the cases when we want to take over the control of the store lifecycle. Typically when we present screens or some flows of the app.
StoreObject<State, Action>
prevents from errors that could occur because of confusion with
Store<State, Action
First of all it follows the rules:
- Actions flow to the root. From child stores to parent
- State changes flow from the root. From Parent to Child stores. From Stores to Subscribers.
- Actions never flow from the root. From parent to child stores.
- Action never flow horizontally. From childStoreA to childStoreB
- Interceptor dispatches new actions to the same store where the initial action was dispatched.
According to the rules above.
When action is dispatched to RootStore:
- action is delivered to root store's reducer
- action is not delivered to child store's reducer
- root state update triggers root store's subscribers
- root state update triggers child stores' subscribers
- Interceptor dispatches additional actions to RootStore
When action is dispatched to ChildStore:
- action is delivered to root store's reducer
- action is delivered to child store's reducer
- root state update triggers root store's subscribers.
- root state update triggers child store's subscribers.
- local state update triggers child stores' subscribers.
- Interceptor dispatches additional actions to ChildStore
- No. Child Store observers are triggered at every state change: both parent's state changes and child's ones.
PureduxStore is licensed under MIT license.