Skip to content
This repository was archived by the owner on May 13, 2020. It is now read-only.

Commit a433bb7

Browse files
committed
Update README the match new Store behaviors
1 parent feeb80e commit a433bb7

File tree

2 files changed

+90
-108
lines changed

2 files changed

+90
-108
lines changed

README.md

Lines changed: 89 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pod 'RxReduce'
7272

7373
# The key principles
7474

75-
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:
7676

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

@@ -81,10 +81,10 @@ Here is a little animation that explains the flow within a state container archi
8181
- The **Store** is the component that handles your state. It has only one input: the "**dispatch()**" function, that takes an **Action** as a parameter.
8282
- The only way to trigger a **State** mutation is to call this "**dispatch()**" function.
8383
- **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.
8686
- 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**.
8888

8989
# How to use RxReduce
9090

@@ -95,18 +95,20 @@ Here is a little animation that explains the flow within a state container archi
9595
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.
9696

9797
```swift
98-
import RxReduce
99-
100-
struct DemoState: State, Equatable {
98+
struct TestState: Equatable {
10199
var counterState: CounterState
102-
var usersState: [String]
100+
var userState: UserState
103101
}
104102

105103
enum CounterState: Equatable {
106104
case empty
107-
case increasing (counter: Int)
108-
case decreasing (counter: Int)
109-
case stopped
105+
case increasing (Int)
106+
case decreasing (Int)
107+
}
108+
109+
enum UserState: Equatable {
110+
case loggedIn (name: String)
111+
case loggedOut
110112
}
111113
```
112114

@@ -117,68 +119,60 @@ Making states **Equatable** is not mandatory but it will allow the **Store** not
117119
Actions are simple data types that embed a payload used in the reducers to mutate the state.
118120

119121
```swift
120-
import RxReduce
121-
122-
struct IncreaseAction: Action {
123-
let increment: Int
124-
}
125-
126-
struct DecreaseAction: Action {
127-
let decrement: Int
128-
}
129-
130-
struct AddUserAction: Action {
131-
let user: String
122+
enum AppAction: Action {
123+
case increase(increment: Int)
124+
case decrease(decrement: Int)
125+
case logUser(user: String)
126+
case clear
132127
}
133128
```
134129

135130
### How to declare **Reducers**
136131

137132
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 👍
138133

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.
140135

141136
```swift
142-
import RxReduce
137+
func counterReduce (state: TestState, action: Action) -> CounterState {
143138

144-
func counterReducer (state: DemoState?, action: Action) -> DemoState {
145-
146-
var currentState = state ?? DemoState(counterState: CounterState.empty, usersState: [])
139+
guard let action = action as? AppAction else { return state.counterState }
147140

148141
var currentCounter = 0
149142

150143
// we extract the current counter value from the current state
151-
switch currentState.counterState {
144+
switch state.counterState {
152145
case .decreasing(let counter), .increasing(let counter):
153146
currentCounter = counter
154147
default:
155148
currentCounter = 0
156149
}
157150

158-
// according to the action we create a new state
151+
// according to the action we mutate the counter state
159152
switch action {
160-
case let action as IncreaseAction:
161-
currentState.counterState = .increasing(counter: currentCounter+action.increment)
162-
return currentState
163-
case let action as DecreaseAction:
164-
currentState.counterState = .decreasing(counter: currentCounter-action.decrement)
165-
return currentState
153+
case .increase(let increment):
154+
return .increasing(currentCounter+increment)
155+
case .decrease(let decrement):
156+
return .decreasing(currentCounter-decrement)
157+
case .clear:
158+
return .empty
166159
default:
167-
return currentState
160+
return state.counterState
168161
}
169162
}
170163

171-
func usersReducer (state: DemoState?, action: Action) -> DemoState {
164+
func userReduce (state: TestState, action: Action) -> UserState {
172165

173-
var currentState = state ?? DemoState(counterState: CounterState.empty, usersState: [])
166+
guard let action = action as? AppAction else { return state.userState }
174167

175-
// according to the action we create a new state
168+
// according to the action we mutate the users state
176169
switch action {
177-
case let 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
180174
default:
181-
return currentState
175+
return state.userState
182176
}
183177
}
184178
```
@@ -187,127 +181,115 @@ Each of these **Reducers** will only handle the **Actions** it is responsible fo
187181

188182
### How to declare a **Store**
189183

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:
191185

192186
```swift
193-
let store = Store<DemoState>(withReducers: [counterReducer, usersReducer])
187+
let store = Store<TestState>(withState: TestState(counterState: .empty, userState: .loggedOut))
194188
```
195189

196-
### How to declare a **Middleware**
190+
### How to aggregate sub-State mutations into a whole State
197191

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.
199194

200-
```swift
201-
import RxReduce
195+
We will use functional programming technics to achieve that.
202196

203-
func loggingMiddleware (state: DemoState?, action: Action) {
204-
guard let state = state else {
205-
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:
214199

215200
```swift
216-
let store = Store<DemoState>(withReducers: [counterReducer, usersReducer], withMiddlewares: [loggingMiddleware])
201+
let counterLens = Lens<TestState, CounterState> (get: { testState in return testState.counterState },
202+
set: { (testState, counterState) -> TestState in
203+
var mutableTestState = testState
204+
mutableTestState.counterState = counterState
205+
return mutableTestState
206+
})
217207
```
218208

219-
### Let's put the pieces all together
209+
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).
220210

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
223212

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**:
225214

226215
```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)
234217
```
235218

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.
237220

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.
239224

240225
```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)
244229

245-
usersState.drive(onNext: { (usersState) in
246-
print ("New usersState is \(usersState)")
247-
}).disposed(by: self.disposeBag)
230+
store.register(mutator: counterMutator)
231+
store.register(mutator: userMutator)
248232
```
249233

250234
And now lets mutate the state:
251235

252236
```swift
253-
store.dispatch(action: IncreaseAction(increment: 10))
254-
store.dispatch(action: IncreaseAction(increment: 0))
255-
store.dispatch(action: AddUserAction(user: "Spock"))
256-
```
257-
258-
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 is increasing(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)
266240
```
267241

268-
As we can see, **counterState** has received only one value 👌
269-
270242
## But wait, there's more ...
271243

272244
### List of actions
273245

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-
276246
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).
277247

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

280250
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:
281251

282252
```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 ...
285255
```
286256

287257
The actions declared in the array will be executed sequentially 👌.
288258

289259
### Asynchronicity
290260

291-
Making an Array of 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\> is also 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.
292262

293263
```swift
294-
let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in IncreaseAction(increment: 1) }
295-
store.dispatch(action: increaseAction)
264+
let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }
265+
store.dispatch(action: increaseAction).subscribe ...
296266
```
297267

298-
If we want to compare RxReduce with Redux, this ability to execute async actions would be equivalent to an "Action Creator".
268+
This will dispatch a **AppAction.increase** Action every 1s and mutate the State accordingly.
269+
270+
If we want to compare RxReduce with Redux, this ability to execute async actions would be equivalent to the "**Action Creator**" concept.
299271

300272
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.
301273

302274
```swift
303-
let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in IncreaseAction(increment: 1) }
304-
let decreaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in DecreaseAction(decrement: 1) }
275+
let increaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.increase(increment: 1) }
276+
let decreaseAction = Observable<Int>.interval(1, scheduler: MainScheduler.instance).map { _ in AppAction.decrease(decrement: 1) }
305277
let asyncActions: [Action] = [increaseAction, decreaseAction]
306-
store.dispatch(action: asyncActions)
278+
store.dispatch(action: asyncActions).subscribe ...
307279
```
308280

309281
Conditional Conformance is a very powerful feature.
310282

283+
### One more thing
284+
285+
The **Store** provides a way to "observe" the State mutations from anywhere. All you have to do is to subscribe to the "**state**" property:
286+
287+
```swift
288+
store.state.subscribe(onNext: { appState in
289+
print (appState)
290+
}).disposed(by: self.disposeBag)
291+
```
292+
311293
## Demo Application
312294

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

RxReduceTests/StoreTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class StoreTests: XCTestCase {
1616

1717
private let disposeBag = DisposeBag()
1818

19-
private let counterLens = Lens<TestState, CounterState> (get: { $0.counterState }) { (testState, counterState) -> TestState in
19+
private let counterLens = Lens<TestState, CounterState> (get: { testState in return testState.counterState }) { (testState, counterState) -> TestState in
2020
var mutableTestState = testState
2121
mutableTestState.counterState = counterState
2222
return mutableTestState

0 commit comments

Comments
 (0)