Last active
November 14, 2017 08:52
-
-
Save dani-mp/c571ada2cf065b32e5c3d6eda3f8c06a to your computer and use it in GitHub Desktop.
Swift Playground showcasing the use of ReSwift middleware to perform asynchronous operations, in contrast of other possible approaches.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import PlaygroundSupport | |
import ReSwift | |
// API | |
protocol API { | |
func loadUsers(completion: @escaping (([User]) -> Void)) | |
} | |
class FakeAPI: API { | |
func loadUsers(completion: @escaping (([User]) -> Void)) { | |
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { | |
completion([User(name: "Marine"), User(name: "Daniel")]) | |
} | |
} | |
} | |
// State | |
struct User { | |
let name: String | |
} | |
struct State: StateType { | |
let loading: Bool | |
let users: [User] | |
} | |
// Actions | |
struct LoadUsers: Action {} | |
struct LoadUsersStart: Action {} | |
struct LoadUsersSuccess: Action { | |
let users: [User] | |
} | |
/* | |
Note that using generics you can easily create a set of actions (api request, start, success, failure) for each endpoint of | |
your API, without having to write them all explicitly. | |
*/ | |
// Reducers | |
let initialState = State(loading: false, users: []) | |
func reducer(action: Action, state: State?) -> State { | |
let defaultState = state ?? initialState | |
switch action { | |
case _ as LoadUsersStart: | |
return State(loading: true, users: defaultState.users) | |
case let success as LoadUsersSuccess: | |
return State(loading: false, users: success.users) | |
default: | |
return defaultState | |
} | |
} | |
// Middleware | |
func loggingMiddleware() -> Middleware<State> { | |
return { dispatch, getState in | |
return { next in | |
return { action in | |
print(action) | |
return next(action) | |
} | |
} | |
} | |
} | |
func apiMiddleware(api: API) -> Middleware<State> { // 1 | |
return { dispatch, getState in // 2 | |
return { next in | |
return { action in | |
switch action { | |
case _ as LoadUsers where !(getState()?.loading ?? false): // 3 | |
dispatch(LoadUsersStart()) // 4 | |
api.loadUsers(completion: { users in | |
dispatch(LoadUsersSuccess(users: users)) // 5 | |
}) | |
default: | |
break | |
} | |
return next(action) // 6 | |
} | |
} | |
} | |
} | |
/* | |
1. You can inject any IO into a middleware. Here we're injecting our API, but you can create a middleware to save documents | |
to the file system, access a data base, send analytics... Using proper DI with a protocol, you can mock the dependency to test | |
your middleware in isolation. | |
2. A middleware can both dispatch new actions and access the state, so it's already the best place to conditionaly dispatch | |
and perform side effects, if needed. | |
3. Here we are interested only in a concrete action/set of actions, the ones related with the API. Also, we don't want to | |
call our API again if we're already making the same call, so we just forget about the action, but we're still aware of what | |
happened, and the action will reach the rest of the middleware and the reducers, in case they need it. | |
4 & 5. The middleware can dispatch new actions, so the start/success/failure dance can be encapsulated here. Note that, as | |
mentioned before, this process can be generalized for the whole API, reducing boilerplate. | |
6. Passing along the current action, we make sure other middleware and the reducers get the opportunity to do something with | |
it as well. Note that we could totally swallow it, too, if that's what we want. | |
*/ | |
// App | |
/* | |
The API middleware is the only entity inside the whole application that knows about our API. We don't need to pass our API | |
instance as a dependency accross our app anymore, simplifying things a lot. Even better, we can have one middleware for each | |
entity in our app that performs side effects or deal with async stuff, separating responsibilities. As a final note, the only | |
dependency that our view controllers/views have is the store. Each view will worry about some raw actions dispatched and some | |
substates subscriptions, and that's it. | |
*/ | |
let store = Store<State>(reducer: reducer, state: nil, middleware: [loggingMiddleware(), apiMiddleware(api: FakeAPI())]) | |
store.dispatch(LoadUsers()) | |
/* | |
Uncomment the next line to observe how the conditional dispatch is applied inside the middleware. We just run one load users | |
operation and it didn't finish yet, so the middleware won't hit the API again. | |
*/ | |
//store.dispatch(LoadUsers()) | |
/* | |
There are way more cool stuff middlewares can do. In my apps, I have middleware that dispatches new actions to refresh some | |
parts of the app when certain things happen, or to dispatch a logout action when I get a 401 from an API request, for instance, | |
without the need of promises, listeners, or callbacks, and always in a modular and explicit way (you can always check the | |
sequence of actions dispatched). | |
If we think about it, both ActionCreator's and AsyncActionCreator's in ReSwift try to solve the same problems (conditional | |
dispatches, start/success/failure handling for async stuff, and side effects) in an ad hoc way, populating the store API with | |
different dispatch functions and new types, and making things less simple. | |
I don't find elegant, for instance, dispatching nil to the store because an ActionCreator didn't provide an actual action. I | |
don't think it's a good idea either to return nil from an action creator (like if nothing happened), but having it dispatched | |
actions from the inside. | |
Regarding the new AsyncActionCreator type, its API is really close to the middleware's one: you have access to the current | |
state and the dispatch function, but then its usage is spread across the whole app, instead of having a single place where you | |
perform the side effects using a certain IO entity. The convenience of having a callback from the caller site that is run when | |
the async operation has finished could be better implemented as a microframework, apart from the ReSwift core project, | |
extending the Action protocol with a new one (we can call it Thunk) and a middleware that understands these thunks and knows | |
how to run them. | |
*/ | |
PlaygroundPage.current.needsIndefiniteExecution = true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment