RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.

Related tags

Event RxReduce
Overview
RxReduce Logo
Travis CI Build Status
Frameworks Carthage Compatible CocoaPods Compatible
Platform Platform
Licence License

About

RxReduce is a Reactive implementation of the state container pattern (like Redux). It is based on the simple concepts of state immutability and unidirectionnal data flow.

Architecture concerns

Since a few years there has been a lot, I mean a LOT, of blog posts, tutorials, books, conferences about adapting alternate architecture patterns to mobile applications. The idea behind all those patterns is to provide a better way to:

  • meet the SOLID requirements (Wikipedia)
  • produce a safer code by design
  • make our code more testable

The good old MVC tends to be replaced by MVP, MVVM or VIPER. I won't go into details about these ones as they are well documented. I think MVVM is currently the most trending pattern, mostly because of its similarities with MVC and MVP and its ability to leverage data binding to ease the data flow. Moreover it is pretty easy to be enhanced by a Coordinator pattern and Reactive programming.

Go check this project if you're interested in Reactive Coordinators (RxFlow) đź‘Ś

That said, there is at least one other architecture pattern that stands out a little bit: State Container.

One of the most famous exemple is Redux, but let's not be restrained by a specific implementation.

Some resources about state containers:

The main goals of this pattern are to:

  • expose a clear/reproductible data flow within your application
  • rely on a single source of truth: the state
  • leverage value types to handle the state immutability
  • promote functional programming, as the only way to mutate a state is to apply a free function: the reducer

I find this approach very interesting compared to the more traditional ones, because it takes care of the consistency of your application state. MVC, MVP, MVVM or VIPER help you slice your application into well defined layers but they don't guide you so much when it comes to handle the state of your app.

Reactive programming is a great companion to state container architectures because it can help to:

  • propage the state mutations
  • build asynchronous actions to mutate the state (for networking, persistence, ...)

RxReduce

RxReduce:

  • provides a generic store that can handle all kinds of states
  • exposes state mutation through a Reactive mechanism
  • provides a simple/unified way to mutate the state synchronously and asynchronously via Actions

Installation

Carthage

In your Cartfile:

github "RxSwiftCommunity/RxReduce"

CocoaPods

In your Podfile:

pod 'RxReduce'

The key principles

The core mechanisms of RxReduce are very straightforward:

Here is a little animation that explains the flow within a state container architecture:

StateContainerArchitectureFlow
  • The Store is the component that handles your state. It has only one input: the "dispatch()" function, that takes an Action as a parameter.
  • The only way to trigger a State mutation is to call this "dispatch()" function.
  • Actions are simple types with no business logic. They embed the payload needed to mutate the state
  • Only free and testable functions called Reducers (RxReduce !) can mutate a State. A "reduce()" function takes a State, an Action and returns a mutated State ... that simple. To be precise, a reducer returns a mutated sub-State of the State. In fact, there is one reducer per sub-State of the State. By sub-State, we mean all the properties that compose a State.
  • The Store will make sure you provide one and only one reducer per sub-State. It brings safety and consistency to your application's logic. Each reducer has a well defined scope.
  • Reducers cannot perform asynchronous logic, they can only mutate the state in a synchronous and readable way. Asynchronous work will be taken care of by Reactive Actions.
  • You can be notified of the state mutation thanks to a Observable<State> exposed by the Store.

How to use RxReduce

Code samples

How to declare a State

As the main idea of state containers is about immutability, avoiding reference type uncontrolled propagation and race conditions, a State must be a value type. Structs and Enums are great for that.

struct TestState: Equatable {
    var counterState: CounterState
    var userState: UserState
}

enum CounterState: Equatable {
    case empty
    case increasing (Int)
    case decreasing (Int)
}

enum UserState: Equatable {
    case loggedIn (name: String)
    case loggedOut
}

Making states Equatable is not mandatory but it will allow the Store not to emit new state values if there is no change between 2 actions. So I strongly recommand to conform to Equatable to minimize the number of view refreshes.

How to declare Actions

Actions are simple data types that embed a payload used in the reducers to mutate the state.

enum AppAction: Action {
    case increase(increment: Int)
    case decrease(decrement: Int)
    case logUser(user: String)
    case clear
}

How to declare Reducers

As I said, a reducer is a free function. These kind of functions takes a value, returns an idempotent value, and performs no side effects. Their declaration is not even related to a type definition. This is super convenient for testing đź‘Ť

Here we define two reducers that will take care of their dedicated sub-State. The first one mutates the CounterState and the second one mutates the UserState.

func counterReduce (state: TestState, action: Action) -> CounterState {

    guard let action = action as? AppAction else { return state.counterState }

    var currentCounter = 0

    // we extract the current counter value from the current state
    switch state.counterState {
    case .decreasing(let counter), .increasing(let counter):
        currentCounter = counter
    default:
        currentCounter = 0
    }

    // according to the action we mutate the counter state
    switch action {
    case .increase(let increment):
        return .increasing(currentCounter+increment)
    case .decrease(let decrement):
        return .decreasing(currentCounter-decrement)
    case .clear:
        return .empty
    default:
        return state.counterState
    }
}

func userReduce (state: TestState, action: Action) -> UserState {

    guard let action = action as? AppAction else { return state.userState }

    // according to the action we mutate the users state
    switch action {
    case .logUser(let user):
        return .loggedIn(name: user)
    case .clear:
        return .loggedOut
    default:
        return state.userState
    }
}

Each of these Reducers will only handle the Actions it is responsible for, nothing less, nothing more.

How to declare a Store

RxReduce provides a generic Store that can handle your application's State. You only need to provide an initial State:

let store = Store<TestState>(withState: TestState(counterState: .empty, userState: .loggedOut))

How to aggregate sub-State mutations into a whole State

As we saw: a reducer takes care only of its dedicated sub-State. We will then define a bunch of reducers to handle the whole application's state mutations. So, we need a mechanism to assemble all the mutated sub-State to a consistent State.

We will use functional programming technics to achieve that.

Lenses

A Lens is a generic way to access and mutate a value type in functional programming. It's about telling the Store how to mutate a certain sub-State of the State. For instance the Lens for CounterState would be:

let counterLens = Lens<TestState, CounterState> (get: { testState in return testState.counterState },
                                                 set: { (testState, counterState) -> TestState in
	var mutableTestState = testState
	mutableTestState.counterState = counterState
	return mutableTestState
    })

it's all about defining how to access the CounterState property (the get closure) of the State and how to mutate it (the set closure).

Mutator

A mutator is simply a structure that groups a Reducer and a Lens for a sub-State. Again for the CounterState:

let counterMutator = Mutator<TestState, CounterState>(lens: counterLens, reducer: counterReduce)

A Mutator has everything needed to know how to mutate the CounterState and how to set it to its parent State.

Let's put the pieces all together

After instantiating the Store, you have to register all the Mutators that will handle the State's sub-States.

let store = Store<TestState>(withState: TestState(counterState: .empty, userState: .loggedOut))
let counterMutator = Mutator<TestState, CounterState>(lens: counterLens, reducer: counterReduce)
let userMutator = Mutator<TestState, UserState>(lens: userLens, reducer: userReduce)

store.register(mutator: counterMutator)
store.register(mutator: userMutator)

And now lets mutate the state:

store.dispatch(action: AppAction.increase(increment: 10)).subscribe(onNext: { testState in
	print ("New State \(testState)")
}).disposed(by: self.disposeBag)

But wait, there's more ...

List of actions

Lately, Swift 4.1 has introduced conditional conformance. If you are not familiar with this concept: A Glance at conditional conformance.

Basically it allows to make a generic type conform to a protocol only if the associated inner type also conforms to this protocol.

For instance, RxReduce leverages this feature to make an Array of Actions be an Action to ! Doing so, it is perfectly OK to dispatch a list of actions to the Store like that:

let actions: [Action] = [AppAction.increase(increment: 10), AppAction.decrease(decrement: 5)]
store.dispatch(action: actions).subscribe ...

The actions declared in the array will be executed sequentially đź‘Ś .

Asynchronicity

Making an Array of Actions be an Action itself is neat, but since we're using Reactive Programming, RxReduxe also applies this technic to RxSwift Observables. It provides a very elegant way to dispatch an Observable<Action> to the Store (because Observable<Action> also conforms to Action), making asynchronous actions very simple.

let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }
store.dispatch(action: increaseAction).subscribe ...

This will dispatch a AppAction.increase Action every 1s and mutate the State accordingly.

If we want to compare RxReduce with Redux, this ability to execute async actions would be equivalent to the "Action Creator" concept.

For the record, we could even dispatch to the Store an Array of Observable<Action>, and it will be seen as an Action as well.

let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }
let decreaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.decrease(decrement: 1) }
let asyncActions: [Action] = [increaseAction, decreaseAction]
store.dispatch(action: asyncActions).subscribe ...

Conditional Conformance is a very powerful feature.

One more thing

The Store provides a way to "observe" the State mutations from anywhere. All you have to do is to subscribe to the "state" property:

store.state.subscribe(onNext: { appState in
	print (appState)
}).disposed(by: self.disposeBag)

Demo Application

A demo application is provided to illustrate the core mechanisms, such as asynchronicity, sub states and view state rendering.

Demo Application Demo Application

Tools and dependencies

RxReduce relies on:

  • SwiftLint for static code analysis (Github SwiftLint)
  • RxSwift to expose State and Actions as Observables your app and the Store can react to (Github RxSwift)
  • Reusable in the Demo App to ease the storyboard cutting into atomic ViewControllers (Github Reusable)
Comments
  • Replace 'Array' with 'ContiguousArray'

    Replace 'Array' with 'ContiguousArray'

    Description

    Reducer and Middleware are aliases of closure. closure is Reference Type as apple doc

    Array of reference types should use ContiguousArray. For ContiguousArray, it is described in Apple document as follows.

    If you need an array of reference types and the array does not need to be bridged to NSArray, use ContiguousArray instead of Array Ref: Advice: Use ContiguousArray with reference types when NSArray bridging is unnecessary

    using ContiguousArray may be more efficient and have more predictable performance than Array. Ref: ContiguousArray

    I replaced Array type of reducers and middlewares with ContiguousArray.

    Checklist

    • [x] this PR is based on develop or a 'develop related' branch
    • [x] the commits inside this PR have explicit commit messages
    • [x] the Jazzy documentation has been generated (if needed -> Jazzy RxReduce)
    enhancement 
    opened by shoheiyokoyama 5
  • Any recommended way to make getters?

    Any recommended way to make getters?

    Hi @twittemb,

    Thanks for this awesome lib :)

    I would like to get the current substate. Not via state() driver.

    The stateSubject is private. Any recommended way to obtain current state?

    Thanks, Derek

    opened by Derek-X-Wang 3
  • Documentation is not up to date with 0.7

    Documentation is not up to date with 0.7

    Hi,

    the README and the example project do not reflect the latest changes on the library, any plans on updating them?

    Also the changes seem quite "major" in between the version, in future do you expect more breaking changes, or do you feel like the implementation is stable now?

    Regards Daniel

    opened by dp-1a 1
  • Compiling RxReduce in Xcode 10.2 with Toolchain 4.2 fails

    Compiling RxReduce in Xcode 10.2 with Toolchain 4.2 fails

    RxReduce 0.9 build is failing with Swift Compiler Error, Command failed due to signal: Abort trap: 6

    This is reproduced as follows

    • Clone from master branch
    • carthage update
    • Download and install Swift Toolchain 4.2 (all 4.2 versions has the same build result)
    • Clean and Build the project
    • The build fails compiling Action.swift

    Result:

    Assertion failed: (hasVal), function operator*, file /Users/buildnode/jenkins/workspace/oss-swift-4.2-package-osx/llvm/include/llvm/ADT/Optional.h, line 145. 0 swift 0x000000010d6fb208 llvm::sys::PrintStackTrace(llvm::raw_ostream&) + 40 1 swift 0x000000010d6fa467 llvm::sys::RunSignalHandlers() + 39 2 swift 0x000000010d6fb882 SignalHandler(int) + 258 3 libsystem_platform.dylib 0x00007fff7254bb5d _sigtramp + 29 4 libsystem_platform.dylib 0x00000000000038b8 _sigtramp + 2376826232 5 libsystem_c.dylib 0x00007fff7240b6a6 abort + 127 6 libsystem_c.dylib 0x00007fff723d420d basename_r + 0 7 swift 0x000000010b163352 _ZN5swift23GenericSignatureBuilder19checkConstraintListINS_4TypeES2_EENS0_10ConstraintIT_EENS_12ArrayRefViewIS2_PNS_20GenericTypeParamTypeEL_ZNS_16staticCastHelperIS7_EEPS4_RKS2_ELb1EEERNSt3__16vectorIS5_NSE_9allocatorIS5_EEEEN4llvm12function_refIFbRKS5_EEENSL_IFNS0_18ConstraintRelationESN_EEENSK_8OptionalINS_4DiagIJjS2_T0_SV_EEEEENSU_IJS2_SV_EEENSU_IJjS2_SV_EEENSL_IFSV_RKS4_EEEb + 1522 8 swift 0x000000010b15d6e0 swift::GenericSignatureBuilder::checkSuperclassConstraints(swift::ArrayRefView<swift::Type, swift::GenericTypeParamType*, swift::GenericTypeParamType* swift::staticCastHelper<swift::GenericTypeParamType>(swift::Type const&), true>, swift::GenericSignatureBuilder::EquivalenceClass*) + 352 9 swift 0x000000010b15bd53 swift::GenericSignatureBuilder::finalize(swift::SourceLoc, swift::ArrayRefView<swift::Type, swift::GenericTypeParamType*, swift::GenericTypeParamType* swift::staticCastHelper<swift::GenericTypeParamType>(swift::Type const&), true>, bool) + 707 10 swift 0x000000010b1647cb swift::GenericSignatureBuilder::computeGenericSignature(swift::SourceLoc, bool, bool) && + 59 11 swift 0x000000010b1ac273 swift::RequirementEnvironment::RequirementEnvironment(swift::DeclContext*, swift::GenericSignature*, swift::ProtocolDecl*, swift::ClassDecl*, swift::ProtocolConformance*) + 2387 12 swift 0x000000010ae216dd swift::matchWitness(swift::TypeChecker&, swift::ProtocolDecl*, swift::ProtocolConformance*, swift::DeclContext*, swift::ValueDecl*, swift::ValueDecl*) + 365 13 swift 0x000000010ae22490 swift::WitnessChecker::findBestWitness(swift::ValueDecl*, bool*, swift::NormalProtocolConformance*, llvm::SmallVectorImpl<swift::RequirementMatch>&, unsigned int&, unsigned int&, bool&) + 656 14 swift 0x000000010ae2aa90 swift::ConformanceChecker::resolveWitnessViaLookup(swift::ValueDecl*) + 672 15 swift 0x000000010ae26e39 swift::ConformanceChecker::checkConformance(swift::MissingWitnessDiagnosisKind) + 713 16 swift 0x000000010ae2573a swift::MultiConformanceChecker::checkIndividualConformance(swift::NormalProtocolConformance*, bool) + 8346 17 swift 0x000000010ae234f8 swift::MultiConformanceChecker::checkAllConformances() + 136 18 swift 0x000000010ae2ed1d swift::TypeChecker::checkConformancesInContext(swift::DeclContext*, swift::IterableDeclContext*) + 3133 19 swift 0x000000010ae7d876 typeCheckFunctionsAndExternalDecls(swift::SourceFile&, swift::TypeChecker&) + 182 20 swift 0x000000010ae7e868 swift::performTypeChecking(swift::SourceFile&, swift::TopLevelContext&, swift::OptionSet<swift::TypeCheckingFlags, unsigned int>, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int) + 1832 21 swift 0x000000010aaa8fd2 swift::CompilerInstance::parseAndCheckTypes(swift::CompilerInstance::ImplicitImports const&) + 834 22 swift 0x000000010aaa8868 swift::CompilerInstance::performSema() + 472 23 swift 0x0000000109f2a5d2 performCompile(swift::CompilerInstance&, swift::CompilerInvocation&, llvm::ArrayRef<char const*>, int&, swift::FrontendObserver*, swift::UnifiedStatsReporter*) + 1410 24 swift 0x0000000109f29042 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 2946 25 swift 0x0000000109ee3e08 main + 1128 26 libdyld.dylib 0x00007fff723663d5 start + 1 27 libdyld.dylib 0x0000000000000058 start + 2378800260

    opened by jacob-zx-fs 0
  • value property on Store class is needed

    value property on Store class is needed

    Hi Currently store class has a property "state" which is Observable. Should it also provide a property "value" which data type is State?

    Best Regards Haojie

    opened by paulxi 0
  • How to support Collection state

    How to support Collection state

    Hi The demo app is very useful, and it helps me a lot to understand the api usage.

    Now I'm facing a problem when I use RxReduce into my project. Let me still use the demo app as example, if UX wants to change the current interaction behavior, instead of using model presentation to display movie detail view, the new app uses navigation presentation to display movie detail, and in the movie detail view, it has a new button 'Next', so without navigate back to movie list view, user can use it to navigate to the next movie detail view, then in this case the app creates 2 MovieDetailViewController.

    My assumption is AppState needs to have an array or dictionary of MovieDetailState, but I don't know how to implement Lens to support this functionality.

    Best Regards Paul

    opened by paulxi 0
  • How to register deep struct mutator?

    How to register deep struct mutator?

    Hi I have a question regarding to update deep struct.

    e.g. The root state is AppState, and it contains a struct property SubState, and the SubState contains a enum property SubSubState.

    I can declare 2 Mutator:

    1. Mutator<AppState, SubState> with Lens<AppState, SubState> and Reducer(state AppState, action: Action)->SubState
    2. Mutator<SubState, SubSubState> with Lens<SubState, SubSubState> and Reducer(state SubState, action: Action)->SubSubState

    But the Store register api only support case 1, any suggestion for updating deep struct?

    Best Regards Paul

    opened by paulxi 0
Releases(0.10)
Owner
RxSwift Community
RxSwift ecosystem projects
RxSwift Community
TopicEventBus is Easy to use, type safe way of implementing Publish–subscribe design pattern.

TopicEventBus Publish–subscribe design pattern implementation framework, with ability to publish events by topic. (NotificationCenter extended alterna

Matan Abravanel 55 Nov 29, 2021
Reactive extensions to Cocoa frameworks, built on top of ReactiveSwift.

ReactiveSwift offers composable, declarative and flexible primitives that are built around the grand concept of streams of values over time. These primitives can be used to uniformly represent common Cocoa and generic programming patterns that are fundamentally an act of observation.

null 20k Jan 3, 2023
A library for reactive and unidirectional Swift applications

ReactorKit is a framework for a reactive and unidirectional Swift application architecture. This repository introduces the basic concept of ReactorKit

ReactorKit 2.5k Dec 28, 2022
RxXRepository: reactive extension for XRepository

RxXRepository is reactive extension for XRepository. This extension allows imple

Sashko Potapov 1 Jan 6, 2022
📬 A lightweight implementation of an observable sequence that you can subscribe to.

Features Lightweight Observable is a simple implementation of an observable sequence that you can subscribe to. The framework is designed to be minima

Felix M. 133 Aug 17, 2022
When is a lightweight implementation of Promises in Swift

Description When is a lightweight implementation of Promises in Swift. It doesn't include any helper functions for iOS and OSX and it's intentional, t

Vadym Markov 259 Dec 29, 2022
VueFlux is the architecture to manage state with unidirectional data flow for Swift, inspired by Vuex and Flux.

Unidirectional State Management Architecture for Swift - Inspired by Vuex and Flux Introduction VueFlux is the architecture to manage state with unidi

Ryo Aoyama 324 Dec 17, 2022
Predictable state management for SwiftUI applications.

SwiftDux Predictable state management for SwiftUI applications. SwiftDux is a state container inspired by Redux and built on top of Combine and SwiftU

Steven Lambion 148 Jul 4, 2022
A simple and predictable state management library inspired by Flux + Elm + Redux.

A simple and predictable state management library inspired by Flux + Elm + Redux. Flywheel is built on top of Corotuines using the concepts of structured concurrency. At the core, lies the State Machine which is based on actor model.

Abhi Muktheeswarar 35 Dec 29, 2022
Promises simplify asynchronous programming, freeing you up to focus on the more important things

Promises simplify asynchronous programming, freeing you up to focus on the more important things. They are easy to learn, easy to master and result in

Max Howell 14k Jan 5, 2023
Open source implementation of Apple's Combine framework for processing values over time.

OpenCombine Open-source implementation of Apple's Combine framework for processing values over time. The main goal of this project is to provide a com

OpenCombine 2.4k Dec 26, 2022
EventBroadcaster is a lightweight event handler framework, written in swift for iOS, macOS, tvOS & watchOS applications.

EventBroadcaster is a lightweight event handler framework, written in swift for iOS, macOS, tvOS & watchOS applications.

Ali Samaiee 4 Oct 5, 2022
ReSwift is a Redux-like implementation of the unidirectional data flow architecture in Swift.

ReSwift is a Redux-like implementation of the unidirectional data flow architecture in Swift. ReSwift helps you to separate three important concerns of your app's components.

null 7.3k Jan 9, 2023
Very simple Observable and Publisher implementation for iOS apps.

Very simple Observable and Publisher implementation for iOS apps.

Igor Kulman 7 Jun 11, 2022
Lightweight Promises for Swift & Obj-C

Tomorrowland Tomorrowland is an implementation of Promises for Swift and Objective-C. A Promise is a wrapper around an asynchronous task that provides

Lily Ballard 115 Nov 23, 2022
Observable is the easiest way to observe values in Swift.

Observable is the easiest way to observe values in Swift. How to Create an Observable and MutableObservable Using MutableObservable you can create and

Robert-Hein Hooijmans 368 Nov 9, 2022
Bond is a Swift binding framework that takes binding concepts to a whole new level.

Bond, Swift Bond Update: Bond 7 has been released! Check out the migration guide to learn more about the update. Bond is a Swift binding framework tha

Declarative Hub 4.2k Jan 5, 2023
UI event handling using Apple's combine framework.

Description Combinative is a library for UI event handling using Apple's combine framework. It doesn't need many dependencies because it is written ma

noppefoxwolf 106 Jan 29, 2022
Swift Apps in a Swoosh! A modern framework for creating iOS apps, inspired by Redux.

Katana is a modern Swift framework for writing iOS applications' business logic that are testable and easy to reason about. Katana is strongly inspire

Bending Spoons 2.2k Jan 1, 2023