If you find yourself having trouble to determine in wich state is your loaded data is before doing a computation on it, or if it happen to often that you code fail because it is optimisitic about the present of such data.
Please consider the following documentation and this typescript library.
## Reference https://github.com/krisajenkins/remotedata http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html
yarn add ...
The whole idea is that a RemoteData can be in only four states :
- NotAsked
- Loading
- Failure error
- Success data
Lets consider that we have a single page app that load a list of post from a remote webservice.
// first thing import remotedata
import * as RemoteData from "./remotedata";
type Post = {
id: number;
userId: number;
title: string;
body: string;
};
// declare the type of your State
type State = {
posts: RemoteData.RemoteData<string, Post[]>;
};
// lets initialize our state holding value
const state: State = {
posts: RemoteData.of()
};
// for each of this state we must prepare an appropiate response
const renderNotAsked = "Data not asked yet";
const renderLoading = "Data is loading !";
// since the state error hold an error value
// success a success value we use a function to extract it
const renderError = (error: string) =>
`Data loading ended up in error ${error}`;
const renderData = JSON.stringify;
document.getElementById("app").innerHTML = state.posts.fold(
renderNotAsked,
renderLoading,
renderError,
renderData
);
The expected result since we just created our remoteData
value will be "Data not asked yet"
// if we change our remotedata status
const newState = {...state, posts: RemoteData.loading()};
// and reaply our render function to it
document.getElementById("app").innerHTML = newState.posts.fold(
renderNotAsked,
renderLoading,
renderError,
renderData
);
// => "Data is loading !"
// and if we go even further
const evenNewerState = {
...newState,
posts: RemoteData.success([
{ id: 1, userId: 2, title: "title", body: "body" }
])
};
document.getElementById("app").innerHTML = evenNewerState.posts.fold(
renderNotAsked,
renderLoading,
renderError,
renderData
);
// => [{"id":1,"userId":2,"title":"title","body":"body"}]
in a real world we would use something like fetch to retrieve our remote data
Most of the time you need to compute data from what you fetch may it be a simple blog post count or a complex datavisualisation.
What can happen when your remote data is not yet available or will not be available at all (for example after a fetch failure) is that your code can fail, such failure can have numerous consequence depending on how you handle unexpected thing in your code.
As such data computed from a RemoteData should be considered remote data itself, RemoteData
have your back here.
type ListOfPostType = RemoteData<string, Post[]>;
// lets consider that we have a Succesfull RemoteData
const remoteData = RemoteData.success([...aListOfPosts]);
const postCount = remoteData.map(listOfPosts => listOfPost.length);
// Success(100)
// lets say we want to filter post by an author id
const filteredList = remoteData.map(
listOfPosts => listOfPost.filter(post => post.userId === 1)
);
// Success([...filteredListOfPosts])
We can see above that the remoteData container can be mapped over, meaning that we can transform its inner value without changing its context.
But what happen if we do the same thing on context where the data does not exisit ?
type ListOfPostType = RemoteData<string, Post[]>;
// lets consider that we have a Succesfull RemoteData
const remoteData = RemoteData.loading([...aListOfPosts]);
const postCount = remoteData.map(listOfPosts => listOfPost.length);
// Loading
// lets say we want to filter post by an author id
const filteredList = remoteData.map(
listOfPosts => listOfPost.filter(post => post.userId === 1)
);
// Loading
On the three context where the data does not exist, the mapped function was not applyed by RemoteData and the current context was returned unchanged. So we can safely create a pipeline of operation for the optimistic (the data exist) case without it failling on us at runtime.
This mean we can avoid taking into account those case in our processing code and that we can handle it in the same fashion when we render this data.
Simpler and safer code indeed.
What happen when we need to handle multiple source of data to create one concrete result for our user to enjoy ?
Ok lets keep using our blog example by assuming that Users data is loaded by another fetch call on a different webservice. And that we want on our blogpost list show their username as author instead of displaying a meaningless userid.
// first thing import remotedata
import * as RemoteData from "./remotedata";
type Post = {
id: number;
userId: number;
title: string;
body: string;
};
type User = {
id: number,
name: string,
}
// declare the type of your State
type State = {
posts: RemoteData.RemoteData<string, Post[]>,
users: RemoteData.RemoteData<string, User[]>,
};
// lets initialize our state holding value
const state: State = {
posts: RemoteData.of(),
users: RemoteData.of(),
};
// first lets build a function that will take the two data and return our computation.
const aggregatePostsAndUsers = posts => users => {
return posts.map(post => {
const user = users.find(user => user.id === post.userId);
return {...post, author: user.name}
});
}
// we can apply a map function to two remoteData
const aggregatedPostsAndUsers = RemoteData.map2(state.post, state.users, aggregatePostsAndUsers);
// NotAsked
Note the mapping function must expect to receive arguments one by one.
This generate a new RemoteData with the computed value associated. The context of the provided RemoteData is used to compute the new context returned.
remoteData1 | remoteData2 | Result |
---|---|---|
Success | Success | Success |
Failure | any | Failure |
any | Failure | Failure |
Failure1 | Failure2 | Failure1 |
Loading | NotAsked or Success | Loading |
NotAsked or Success | Loading | Loading |
NotAsked | NotAsked | NotAsked |
RemoteData<E, T>::notAsked: void -> boolean
RemoteData<E, T>::loading: void -> boolean
RemoteData<E, T>::failure: void -> boolean
RemoteData<E, T>::success: void -> boolean
RemoteData<E, T>::map: (T -> B) -> RemoteData<E, B>
RemoteData<E, T>::bimap: (E -> F) -> (T -> B) -> RemoteData<F,B>
RemoteData<E, T>::getOrElse: T -> T
RemoteData<E, T>::fold: B -> B -> (E -> B) -> (T -> B ) -> B
RemoteData<E, T>::reduce: B -> ((B, T) -> B) -> B
RemoteData<E, T>::chain: (T -> RemoteData<E, B>) -> RemoteData<E, B>
RemoteData<E, T>::extend: (RemoteData<E, T> -> B) -> RemoteData<E, B>
notAsked<E, T>: void -> RemoteData<E, T>
loading<E, T>: void -> RemoteData<E, T>
failure<E, T>: E -> RemoteData<E, T>
success<E, T>: T -> RemoteData<E, T>
isNotAsked: RemoteData<E, T> -> boolean
isLoading: RemoteData<E, T> -> boolean
isFailure: RemoteData<E, T> -> boolean
isSuccess: RemoteData<E, T> -> boolean
map: RemoteData<E, T> -> (T -> B) -> RemoteData<E, B>
bimap: RemoteData<E, A> -> (E -> F) -> (A -> B) -> RemoteData<F, B>
ap: RemoteData<E, A> -> RemoteData<E, (A -> B)> -> RemoteData<E, B>
map2: RemoteData<E, A> -> RemoteData<E, B> -> (A -> B -> C) -> RemoteData<E, C>
map3: RemoteData<E, A> -> RemoteData<E, B> -> RemoteData<E, C> -> (A -> B -> C -> D) -> RemoteData<E, D>
append: RemoteData<E, A> -> RemoteData<E, B> -> RemoteData<E, [A, B]>
reduce: RemoteData<E, A> -> B -> ((B, A) -> B) -> B
chain: RemoteData<E, A> -> (A -> RemoteData<E, B>) -> RemoteData<E, B>
extend: RemoteData<E, A> -> (RemoteData<E, A> -> B) -> RemoteData<E, B>
yarn && yarn start
need the following typedef for ramda
interface Apply<T> {
ap<U>(f: Apply<(value: A) => U>): Apply<U>
}
interface Chain<T> {
chain<U>(fn: (t: T) => Chain<U>): Chain<U>;
}
ap<T, U>(fb: Apply<T>, fa: Apply<(n: T) => U>): Apply<U>; // used for applicative functors
ap<T, U>(fb: Apply<T>): (fa: Apply<(n: T) => U>) => Apply<U>; // used for applicative functors
chain<T, U>(fn: (n: T) => U, obj: Chain<T>): Chain<U>; // used in chains
chain<T, U>(fn: (n: T) => U): (obj: Chain<T>) => Chain<U>; // used in chains