You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on May 13, 2020. It is now read-only.
The core mechanisms of **RxReduce** are very straightforward (if you know Redux, you won't be disturbed):
75
+
The core mechanisms of **RxReduce** are very straightforward:
76
76
77
77
Here is a little animation that explains the flow within a state container architecture:
78
78
@@ -81,10 +81,10 @@ Here is a little animation that explains the flow within a state container archi
81
81
- The **Store** is the component that handles your state. It has only one input: the "**dispatch()**" function, that takes an **Action** as a parameter.
82
82
- The only way to trigger a **State** mutation is to call this "**dispatch()**" function.
83
83
-**Actions** are simple types with no business logic. They embed the payload needed to mutate the **state**
84
-
- Only free and testable functions called **Reducers** (RxReduce !) can mutate a **State**. A "**reduce()**" function takes a **State**, an **Action** and returns the new**State** ... that simple.
85
-
-You can have as many reducers as you want, they will be applied by the **Store**'s "**dispatch()**" function sequentially. It could be nice to have a reducer per business concern for instance.
84
+
- 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.
85
+
-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.
86
86
- 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**.
87
-
- You can be notified of the state mutation thanks to a **Driver\<State\>** exposed by the **Store**.
87
+
- You can be notified of the state mutation thanks to a **Observable\<State\>** exposed by the **Store**.
88
88
89
89
# How to use RxReduce
90
90
@@ -95,18 +95,20 @@ Here is a little animation that explains the flow within a state container archi
95
95
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.
96
96
97
97
```swift
98
-
importRxReduce
99
-
100
-
structDemoState: State, Equatable{
98
+
structTestState: Equatable{
101
99
var counterState: CounterState
102
-
varusersState: [String]
100
+
varuserState: UserState
103
101
}
104
102
105
103
enumCounterState: Equatable{
106
104
caseempty
107
-
caseincreasing (counter: Int)
108
-
casedecreasing (counter: Int)
109
-
casestopped
105
+
caseincreasing (Int)
106
+
casedecreasing (Int)
107
+
}
108
+
109
+
enumUserState: Equatable{
110
+
caseloggedIn (name: String)
111
+
caseloggedOut
110
112
}
111
113
```
112
114
@@ -117,68 +119,60 @@ Making states **Equatable** is not mandatory but it will allow the **Store** not
117
119
Actions are simple data types that embed a payload used in the reducers to mutate the state.
118
120
119
121
```swift
120
-
importRxReduce
121
-
122
-
structIncreaseAction: Action {
123
-
let increment: Int
124
-
}
125
-
126
-
structDecreaseAction: Action {
127
-
let decrement: Int
128
-
}
129
-
130
-
structAddUserAction: Action {
131
-
let user: String
122
+
enumAppAction: Action {
123
+
caseincrease(increment: Int)
124
+
casedecrease(decrement: Int)
125
+
caselogUser(user: String)
126
+
caseclear
132
127
}
133
128
```
134
129
135
130
### How to declare **Reducers**
136
131
137
132
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 👍
138
133
139
-
Here we define 2**reducers** that will be applied in sequence each time an Action is dispatched in the **Store**.
134
+
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.
// according to the action we mutate the users state
176
169
switch action {
177
-
caselet action as AddUserAction:
178
-
currentState.usersState.append(action.user)
179
-
return currentState
170
+
case .logUser(let user):
171
+
return .loggedIn(name: user)
172
+
case .clear:
173
+
return .loggedOut
180
174
default:
181
-
returncurrentState
175
+
returnstate.userState
182
176
}
183
177
}
184
178
```
@@ -187,127 +181,115 @@ Each of these **Reducers** will only handle the **Actions** it is responsible fo
187
181
188
182
### How to declare a **Store**
189
183
190
-
**RxReduce** provides a concrete Store type. A Store needs at leat one **Reducer**:
184
+
**RxReduce** provides a generic **Store** that can handle your application's State. You only need to provide an initial State:
191
185
192
186
```swift
193
-
let store = Store<DemoState>(withReducers: [counterReducer, usersReducer])
187
+
let store = Store<TestState>(withState: TestState(counterState: .empty, userState: .loggedOut))
194
188
```
195
189
196
-
### How to declare a **Middleware**
190
+
### How to aggregate sub-State mutations into a whole State
197
191
198
-
Middlewares are very similar to Reducers BUT they cannot mutate the state. They are some kind of "passive observers" of what's being dispatched in the store. Middlewares can be used for logging, analytics, state recording, ...
192
+
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.
193
+
So, we need a mechanism to assemble all the mutated sub-State to a consistent State.
199
194
200
-
```swift
201
-
importRxReduce
195
+
We will use functional programming technics to achieve that.
print ("A new Action \(action) will provide a first value for an empty state")
206
-
return
207
-
}
208
-
209
-
print ("A new Action \(action) will mutate current State : \(state)")
210
-
}
211
-
```
212
-
213
-
A Store initializer takes Reducers and if needed, an Array of Middlewares as well:
197
+
#### Lenses
198
+
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:
214
199
215
200
```swift
216
-
let store = Store<DemoState>(withReducers: [counterReducer, usersReducer], withMiddlewares: [loggingMiddleware])
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).
220
210
221
-
RxReduce allows to listen to the whole state or to some of its properties (we may call them **substates**).
222
-
Listening only to substates makes sense when the state begins to be huge and you do not want to be notified each time one of its portion has been modified.
211
+
#### Mutator
223
212
224
-
First we pick the substate we want to observe (by passing a closure to the **store.state()** function):
213
+
A mutator is simply a structure that groups a **Reducer** and a **Lens** for a sub-State. Again for the **CounterState**:
225
214
226
215
```swift
227
-
let counterState: Driver<CounterState> = store.state { (demoState) -> CounterState in
228
-
return demoState.counterState
229
-
}
230
-
231
-
let usersState: Driver<[String]> = store.state { (demoState) -> [String] in
232
-
return demoState.usersState
233
-
}
216
+
let counterMutator = Mutator<TestState, CounterState>(lens: counterLens, reducer: counterReduce)
234
217
```
235
218
236
-
The trailing closure gives you the whole state of the **Store**, and you just have to **extract** the substate you want to observe.
219
+
A Mutator has everything needed to know how to mutate the CounterState and how to set it to its parent State.
237
220
238
-
Then subscribe to the substate:
221
+
### Let's put the pieces all together
222
+
223
+
After instantiating the Store, you have to register all the Mutators that will handle the State's sub-States.
239
224
240
225
```swift
241
-
counterState.drive(onNext: { (counterState) in
242
-
print ("New counterState is \(counterState)")
243
-
}).disposed(by: self.disposeBag)
226
+
let store = Store<TestState>(withState: TestState(counterState: .empty, userState: .loggedOut))
227
+
let counterMutator = Mutator<TestState, CounterState>(lens: counterLens, reducer: counterReduce)
228
+
let userMutator = Mutator<TestState, UserState>(lens: userLens, reducer: userReduce)
Please notice that the second action will not modify the state, and the **Store** will be smart about that and will not trigger a new value for the **counterState**. This happens only because **DemoState** conforms to **Equatable**.
259
-
260
-
The output will be:
261
-
262
-
```swift
263
-
New counterState isincreasing(10) (for the first action)
264
-
New usersState is [] (for the first action)
265
-
New usersState is ["Spock"] (for the third action)
237
+
store.dispatch(action: AppAction.increase(increment: 10)).subscribe(onNext: { testState in
238
+
print ("New State \(testState)")
239
+
}).disposed(by: self.disposeBag)
266
240
```
267
241
268
-
As we can see, **counterState** has received only one value 👌
269
-
270
242
## But wait, there's more ...
271
243
272
244
### List of actions
273
245
274
-
RxReduce is a lightweight framework. Pretty much everything is a protocol (except the Store, but if you want to implement you own Store it is perfectly fine since RxReduce provides a StoreType protocol you can conform to).
275
-
276
246
Lately, 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).
277
247
278
248
Basically it allows to make a generic type conform to a protocol only if the associated inner type also conforms to this protocol.
279
249
280
250
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:
281
251
282
252
```swift
283
-
let actions: [Action] = [IncreaseAction(increment: 10), DecreaseAction(increment: 5)]
284
-
store.dispatch(action: actions)
253
+
let actions: [Action] = [AppAction.increase(increment: 10), AppAction.decrease(decrement: 5)]
254
+
store.dispatch(action: actions).subscribe...
285
255
```
286
256
287
257
The actions declared in the array will be executed sequentially 👌.
288
258
289
259
### Asynchronicity
290
260
291
-
Making an Arrayof Actions be an Action itself is neat, but since we're using Reactive Programming, RxReduxe also applies this technic to Observables. It provides a very elegant way to dispatch an Observable\<Action\> to the Store (because Observable\<Action\>isalso an Action), making asynchronous actions very simple.
261
+
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.
292
262
293
263
```swift
294
-
let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _inIncreaseAction(increment: 1) }
295
-
store.dispatch(action: increaseAction)
264
+
let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _inAppAction.increase(increment: 1) }
0 commit comments