{"id":1836,"url":"https://github.com/RxSwiftCommunity/RxReduce","last_synced_at":"2025-08-02T05:32:53.587Z","repository":{"id":62453178,"uuid":"130613730","full_name":"RxSwiftCommunity/RxReduce","owner":"RxSwiftCommunity","description":"RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.","archived":true,"fork":false,"pushed_at":"2019-04-24T17:56:25.000Z","size":5623,"stargazers_count":125,"open_issues_count":4,"forks_count":11,"subscribers_count":10,"default_branch":"master","last_synced_at":"2024-05-09T08:04:42.948Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RxSwiftCommunity.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-04-22T22:41:36.000Z","updated_at":"2023-09-12T12:33:00.000Z","dependencies_parsed_at":"2022-11-01T23:45:57.065Z","dependency_job_id":null,"html_url":"https://github.com/RxSwiftCommunity/RxReduce","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RxSwiftCommunity%2FRxReduce","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RxSwiftCommunity%2FRxReduce/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RxSwiftCommunity%2FRxReduce/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RxSwiftCommunity%2FRxReduce/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RxSwiftCommunity","download_url":"https://codeload.github.com/RxSwiftCommunity/RxReduce/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228443616,"owners_count":17920760,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-01-05T20:15:56.955Z","updated_at":"2024-12-06T09:30:25.665Z","avatar_url":"https://github.com/RxSwiftCommunity.png","language":"Swift","funding_links":[],"categories":["Reactive Programming","Libs","Libraries","Events [🔝](#readme)"],"sub_categories":["Prototyping","Events","Other Parsing"],"readme":"| \u003cimg alt=\"RxReduce Logo\" src=\"https://raw.githubusercontent.com/RxSwiftCommunity/RxReduce/develop/Resources/RxReduce_Logo.png\" width=\"250\"/\u003e | \u003cul align=\"left\"\u003e\u003cli\u003e\u003ca href=\"#about\"\u003eAbout\u003c/a\u003e\u003cli\u003e\u003ca href=\"#architecture-concerns\"\u003eArchitecture concerns\u003c/a\u003e\u003cli\u003e\u003ca href=\"#rxreduce\"\u003eRxReduce\u003c/a\u003e\u003cli\u003e\u003ca href=\"#installation\"\u003eInstallation\u003c/a\u003e\u003cli\u003e\u003ca href=\"#the-key-principles\"\u003eThe key principles\u003c/a\u003e\u003cli\u003e\u003ca href=\"#how-to-use-rxreduce\"\u003eHow to use RxReduce\u003c/a\u003e\u003cli\u003e\u003ca href=\"#tools-and-dependencies\"\u003eTools and dependencies\u003c/a\u003e\u003c/ul\u003e |\n| -------------- | -------------- |\n| Travis CI | [![Build Status](https://travis-ci.org/RxSwiftCommunity/RxReduce.svg?branch=develop)](https://travis-ci.org/RxSwiftCommunity/RxReduce) |\n| Frameworks | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/RxReduce.svg?style=flat)](http://cocoapods.org/pods/RxReduce) |\n| Platform | [![Platform](https://img.shields.io/cocoapods/p/RxReduce.svg?style=flat)](http://cocoapods.org/pods/RxReduce) |\n| Licence | [![License](https://img.shields.io/cocoapods/l/RxReduce.svg?style=flat)](http://cocoapods.org/pods/RxReduce) |\n\n\u003cspan style=\"float:none\" /\u003e\n\n# About\nRxReduce 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.\n\n# Architecture concerns\nSince 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:\n\n- meet the SOLID requirements ([Wikipedia](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)))\n- produce a safer code by design\n- make our code more testable\n\nThe 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.\n\nGo check this project if you're interested in Reactive Coordinators ([RxFlow](https://github.com/RxSwiftCommunity/RxFlow)) 👌\n\nThat said, there is at least one other architecture pattern that stands out a little bit: **State Container**.\n\nOne of the most famous exemple is Redux, but let's not be restrained by a specific implementation.\n\nSome resources about state containers:\n\n- [State Container](https://jobandtalent.engineering/ios-architecture-an-state-container-based-approach-4f1a9b00b82e)\n- [Unidirectional Data Flow](https://academy.realm.io/posts/benji-encz-unidirectional-data-flow-swift/)\n\nThe main goals of this pattern are to:\n\n- expose a clear/reproductible data flow within your application\n- rely on a single source of truth: the **state**\n- leverage value types to handle the state immutability\n- promote functional programming, as the only way to mutate a state is to apply a free function: **the reducer**\n\nI 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.\n\nReactive programming is a great companion to state container architectures because it can help to:\n\n- propage the state mutations\n- build asynchronous actions to mutate the state (for networking, persistence, ...)\n\n# RxReduce\n\nRxReduce:\n\n- provides a generic store that can handle all kinds of states\n- exposes state mutation through a Reactive mechanism\n- provides a simple/unified way to mutate the state synchronously and asynchronously via Actions\n\n# Installation\n\n## Carthage\n\nIn your Cartfile:\n\n```ruby\ngithub \"RxSwiftCommunity/RxReduce\"\n```\n\n## CocoaPods\n\nIn your Podfile:\n\n```ruby\npod 'RxReduce'\n```\n\n# The key principles\n\nThe core mechanisms of **RxReduce** are very straightforward:\n\nHere is a little animation that explains the flow within a state container architecture:\n\n\u003cimg width=\"500\" alt=\"StateContainerArchitectureFlow\" src=\"https://raw.githubusercontent.com/RxSwiftCommunity/RxReduce/develop/Resources/RxReduceScheme.gif\"/\u003e\n\n- The **Store** is the component that handles your state. It has only one input: the \"**dispatch()**\" function, that takes an **Action** as a parameter.\n- The only way to trigger a **State** mutation is to call this \"**dispatch()**\" function.\n- **Actions** are simple types with no business logic. They embed the payload needed to mutate the **state**\n- 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.\n- 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.\n- 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**.\n- You can be notified of the state mutation thanks to a **Observable\\\u003cState\\\u003e** exposed by the **Store**.\n\n# How to use RxReduce\n\n## Code samples\n\n### How to declare a **State**\n\nAs 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.\n\n```swift\nstruct TestState: Equatable {\n    var counterState: CounterState\n    var userState: UserState\n}\n\nenum CounterState: Equatable {\n    case empty\n    case increasing (Int)\n    case decreasing (Int)\n}\n\nenum UserState: Equatable {\n    case loggedIn (name: String)\n    case loggedOut\n}\n```\n\nMaking 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.  \n\n### How to declare **Actions**\n\nActions are simple data types that embed a payload used in the reducers to mutate the state.\n\n```swift\nenum AppAction: Action {\n    case increase(increment: Int)\n    case decrease(decrement: Int)\n    case logUser(user: String)\n    case clear\n}\n```\n\n### How to declare **Reducers**\n\nAs 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 👍\n\nHere 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.\n\n```swift\nfunc counterReduce (state: TestState, action: Action) -\u003e CounterState {\n\n    guard let action = action as? AppAction else { return state.counterState }\n\n    var currentCounter = 0\n\n    // we extract the current counter value from the current state\n    switch state.counterState {\n    case .decreasing(let counter), .increasing(let counter):\n        currentCounter = counter\n    default:\n        currentCounter = 0\n    }\n\n    // according to the action we mutate the counter state\n    switch action {\n    case .increase(let increment):\n        return .increasing(currentCounter+increment)\n    case .decrease(let decrement):\n        return .decreasing(currentCounter-decrement)\n    case .clear:\n        return .empty\n    default:\n        return state.counterState\n    }\n}\n\nfunc userReduce (state: TestState, action: Action) -\u003e UserState {\n\n    guard let action = action as? AppAction else { return state.userState }\n\n    // according to the action we mutate the users state\n    switch action {\n    case .logUser(let user):\n        return .loggedIn(name: user)\n    case .clear:\n        return .loggedOut\n    default:\n        return state.userState\n    }\n}\n```\n\nEach of these **Reducers** will only handle the **Actions** it is responsible for, nothing less, nothing more. \n\n### How to declare a **Store**\n\n**RxReduce** provides a generic **Store** that can handle your application's State. You only need to provide an initial State:\n\n```swift\nlet store = Store\u003cTestState\u003e(withState: TestState(counterState: .empty, userState: .loggedOut))\n```\n\n### How to aggregate sub-State mutations into a whole State\n\nAs 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. \nSo, we need a mechanism to assemble all the mutated sub-State to a consistent State.\n\nWe will use functional programming technics to achieve that.\n\n#### Lenses\nA 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:\n\n```swift\nlet counterLens = Lens\u003cTestState, CounterState\u003e (get: { testState in return testState.counterState },\n                                                 set: { (testState, counterState) -\u003e TestState in\n\tvar mutableTestState = testState\n\tmutableTestState.counterState = counterState\n\treturn mutableTestState\n    })\n```\n\nit's all about defining how to access the CounterState property (the `get` closure) of the State and how to mutate it (the `set` closure).\n\n#### Mutator\n\nA mutator is simply a structure that groups a **Reducer** and a **Lens** for a sub-State. Again for the **CounterState**:\n\n```swift\nlet counterMutator = Mutator\u003cTestState, CounterState\u003e(lens: counterLens, reducer: counterReduce)\n```\n\nA Mutator has everything needed to know how to mutate the CounterState and how to set it to its parent State.\n\n### Let's put the pieces all together\n\nAfter instantiating the Store, you have to register all the Mutators that will handle the State's sub-States.\n\n```swift\nlet store = Store\u003cTestState\u003e(withState: TestState(counterState: .empty, userState: .loggedOut))\nlet counterMutator = Mutator\u003cTestState, CounterState\u003e(lens: counterLens, reducer: counterReduce)\nlet userMutator = Mutator\u003cTestState, UserState\u003e(lens: userLens, reducer: userReduce)\n\nstore.register(mutator: counterMutator)\nstore.register(mutator: userMutator)\n```\n\nAnd now lets mutate the state:\n\n```swift\nstore.dispatch(action: AppAction.increase(increment: 10)).subscribe(onNext: { testState in\n\tprint (\"New State \\(testState)\")\n}).disposed(by: self.disposeBag)\n```\n\n## But wait, there's more ...\n\n### List of actions\n\nLately, Swift 4.1 has introduced conditional conformance. If you are not familiar with this concept: [A Glance at conditional conformance](https://medium.com/@thibault.wittemberg/a-glance-at-conditional-conformance-c1f2d9ea29a3).\n\nBasically it allows to make a generic type conform to a protocol only if the associated inner type also conforms to this protocol. \n\nFor 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:\n\n```swift\nlet actions: [Action] = [AppAction.increase(increment: 10), AppAction.decrease(decrement: 5)]\nstore.dispatch(action: actions).subscribe ...\n```\n\nThe actions declared in the array will be executed sequentially 👌.\n\n### Asynchronicity\n\nMaking 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\\\u003cAction\\\u003e to the Store (because Observable\\\u003cAction\\\u003e also conforms to Action), making asynchronous actions very simple. \n\n```swift\nlet increaseAction = Observable\u003cInt\u003e.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }\nstore.dispatch(action: increaseAction).subscribe ...\n```\n\nThis will dispatch a **AppAction.increase** Action every 1s and mutate the State accordingly.\n\nIf we want to compare RxReduce with Redux, this ability to execute async actions would be equivalent to the \"**Action Creator**\" concept.\n\nFor the record, we could even dispatch to the Store an Array of Observable\\\u003cAction\\\u003e, and it will be seen as an Action as well.\n\n```swift\nlet increaseAction = Observable\u003cInt\u003e.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }\nlet decreaseAction = Observable\u003cInt\u003e.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.decrease(decrement: 1) }\nlet asyncActions: [Action] = [increaseAction, decreaseAction]\nstore.dispatch(action: asyncActions).subscribe ...\n```\n\nConditional Conformance is a very powerful feature.\n\n### One more thing\n\nThe **Store** provides a way to \"observe\" the State mutations from anywhere. All you have to do is to subscribe to the \"**state**\" property:\n\n```swift\nstore.state.subscribe(onNext: { appState in\n\tprint (appState)\n}).disposed(by: self.disposeBag)\n```\n\n## Demo Application\n\nA demo application is provided to illustrate the core mechanisms, such as asynchronicity, sub states and view state rendering.\n\n\u003ctable\u003e\u003ctr\u003e\u003ctd\u003e\u003cimg style=\"border:2px solid black\" width=\"200\" alt=\"Demo Application\" src=\"https://raw.githubusercontent.com/RxSwiftCommunity/RxReduce/develop/Resources/RxReduceDemo1.png\"/\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cimg style=\"border:2px solid black\" width=\"200\" alt=\"Demo Application\" src=\"https://raw.githubusercontent.com/RxSwiftCommunity/RxReduce/develop/Resources/RxReduceDemo2.png\"/\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\n# Tools and dependencies\n\nRxReduce relies on:\n\n- SwiftLint for static code analysis ([Github SwiftLint](https://github.com/realm/SwiftLint))\n- RxSwift to expose State and Actions as Observables your app and the Store can react to ([Github RxSwift](https://github.com/ReactiveX/RxSwift))\n- Reusable in the Demo App to ease the storyboard cutting into atomic ViewControllers ([Github Reusable](https://github.com/AliSoftware/Reusable))\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FRxSwiftCommunity%2FRxReduce","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FRxSwiftCommunity%2FRxReduce","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FRxSwiftCommunity%2FRxReduce/lists"}