Last active
March 6, 2018 02:10
-
-
Save asdacap/4fec3b341da2b043bbb612fec07a25fb to your computer and use it in GitHub Desktop.
A pattern I like to use in react-redux app.
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
// @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