Skip to content

Instantly share code, notes, and snippets.

@asdacap
Last active March 6, 2018 02:10
Show Gist options
  • Save asdacap/4fec3b341da2b043bbb612fec07a25fb to your computer and use it in GitHub Desktop.
Save asdacap/4fec3b341da2b043bbb612fec07a25fb to your computer and use it in GitHub Desktop.
A pattern I like to use in react-redux app.
// @flow
import * as _ from 'lodash'
import invariant from 'invariant'
/**
* A LoadableResource is a monad that wraps a resource, which may be in the
* four state
* - missing
* - pending
* - error
* - loaded
* Where error and loaded will also have error and resource accordingly.
* A monad is a thing that can have a sequence of transformation/function.
* I'm not even sure if this is considered a monad.
*
* Anyway, this LoadableResource is eager(not lazy) and pure. Each function,
* which essentially call back to flatMap will return another LoadableResource
* that has been transformed. The point of this pattern is easy
* transformation or combination of multiple LoadableResource.
*
* It is being used in a react-redux app that I'm working on. Imagine in your
* store, you have a user and a transaction model where the transaction have
* a user_id field, and both are fetched from the internet. So it is possible
* that the resource is loading (pending), no longer in the server, have error
* while fetching from server or have been fetched (loaded). You need to store
* all four of those possibility for both model, and then combine them and then
* render the react view, checking all four possibilities.
*
* Imagine if you are going to render a Transaction Info page, you are only given
* the transaction_id and you need to also shows the user name which is not stored
* in the Transaction model. You are using React, the render method must be pure,
* so you should not fetch the model there. You are using Redux, so ideally
* the model should be stored in the store, which may already be loaded,
* by something else, who knows. So, you decided that you will store the model
* along with its status in the store. And you will make a redux action that
* will load both model if its not already loaded. You call the action in the
* react constructor or props change or whatever, then its time to render.
*
* You then proceed to check the loaded status of the transaction model,
* and then if its loaded get the user id, fetch from store and then check user's
* loaded status and then return the JSX if both are loaded. What if an error
* occured? What if they are loading? What if only one of them are loading? What
* if the User model have a UserInfo model that have to be fetched separately?
* And you need that too? Of course you could make your own sequence of If-Else,
* but that is cumbersome. With the function in this file, you could make the
* render look like this
*
* render() {
* const store = this.props.store
* const transactionId = this.props.transactionId
* const view = LD.flatMap(store.transactions[transactionId] || LD.missing(), (transaction) => {
* return LD.flatMap(store.users[transacation.userId] || LD.missing(), (user) => {
* return LD.flatMap(store.userInfo[user.id] || LD.missing(), (userInfo) => {
* return LD.of(<View>
* <Text>Transaction ID is {transaction.id}</Text>
* <Text>User name is {user.name}</Text>
* <Text>User address is {userInfo.address}</Text>
* </View>)
* })
* })
* })
*
* if (view.status === 'missing') {
* return <Text>Resource is missing</Text>
* } else if (view.status === 'loading') {
* return <Text>Resource is loading</Text>
* } else if (view.status === 'error') {
* return <Text>Error occurred {view.error}</Text>
* } else {
* return view.resource
* }
* }
*
* If any of the three resource is not loaded, the resulting LoadableResource of the view will not be
* loaded, and the respective status will be forwarded. Check the type of the function to see what it does.
*
* If you want to have more fun you can do:
*
* const view = LD.doM(function * () {
* const transaction = yield (store[transactions[transactionId] || LD.missing())
* const user = yield (store.users[transaction.userId] || LD.missing())
* const userInfo = yield (store.userInfo[user.id] || LD.missing())
*
* return <View>
* <Text>Transaction ID is {transaction.id}</Text>
* <Text>User name is {user.name}</Text>
* <Text>User address is {userInfo.address}</Text>
* </View>
* } ())
*
* Note that it won't typecheck with the generator function. And the 'this' variable may not work in generator function.
*
*/
export type LoadableResource<+T> =
{ status: "missing" } |
{ status: "pending" } |
{ status: "error", error: Error } |
{ status: "loaded", +resource: T }
export function combine3<T, T2, T3, R> (
resource1: LoadableResource<T>,
resource2: LoadableResource<T2>,
resource3: LoadableResource<T3>,
func: (T, T2, T3) => LoadableResource<R>
): LoadableResource<R> {
return flatMap(resource1, (r1) => {
return flatMap(resource2, (r2) => {
return flatMap(resource3, (r3) => {
return func(r1, r2, r3)
})
})
})
}
export function combine<T, T2, R> (
resource1: LoadableResource<T>,
resource2: LoadableResource<T2>,
func: (T, T2) => LoadableResource<R>
): LoadableResource<R> {
return flatMap(resource1, (r1) => {
return flatMap(resource2, (r2) => {
return func(r1, r2)
})
})
}
export function of<T> (resource: T): LoadableResource<T> {
return {
status: 'loaded',
resource
}
}
export function missing<T> (): LoadableResource<T> {
return {
status: 'missing'
}
}
export function pending<T> (): LoadableResource<T> {
return {
status: 'pending'
}
}
export function error<T> (error: any): LoadableResource<T> {
return {
status: 'error',
error: error
}
}
export function flatMap<T, R> (
resource: LoadableResource<T>,
func: (T) => LoadableResource<R>
): LoadableResource<R> {
invariant(resource !== undefined, 'resource must not be undefined')
if (resource.status === 'loaded') {
return func(resource.resource)
} else {
return resource
}
}
export function flattenArray<T> (loadableResourceArr: Array<LoadableResource<T>>): LoadableResource<Array<T>> {
const init: LoadableResource<Array<T>> = {
status: 'loaded',
resource: []
}
const res = _.reduce(loadableResourceArr, (res: LoadableResource<Array<T>>, val: LoadableResource<T>, key) => {
return combine(res, val, (loadableArr: Array<T>, resolvedVal: T): LoadableResource<Array<T>> => {
return {
status: 'loaded',
resource: [...loadableArr, resolvedVal]
}
})
}, init)
return res
}
export function map<T, R> (lResource: LoadableResource<T>, mapper: (T) => R): LoadableResource<R> {
return flatMap(lResource, (r: T) => {
return of(mapper(r))
})
}
export function doM<T> (generator: Generator<LoadableResource<any>, T, any>): LoadableResource<T> {
const runIt = (currentValue): LoadableResource<any> => {
const res = generator.next(currentValue)
const value: any = res.value
if (res.value === undefined) throw Error('Generator must not return undefined')
if (res.done) {
return of(res.value)
} else {
return flatMap(value, runIt)
}
}
return runIt(undefined)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment