Skip to content

Instantly share code, notes, and snippets.

@dani-mp
Last active November 14, 2017 08:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dani-mp/c571ada2cf065b32e5c3d6eda3f8c06a to your computer and use it in GitHub Desktop.
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.
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