This is a library for true asynchronouse programming.
Wrap a promise using the lacy function to turn it into a LacyPromise.
A LacyPromise has all the properties and methods of the result type of the wrapped promise but they will return more LacyPromises.
In short that means instead of
async function getMovieTitle() {
const movie = await fetchMovie()
return movie.title
}one can write
function getMovieTitle() {
const movie = lacy(fetchMovie())
return movie.title
}With standard promises, programmers need to deal with explicit concurrency management: When awaiting each promise in place, data fetching happens in a cascading fashion:
const movie = await fetchMovie()
// fetching the actor will only start after the movie has been fetched.
const actor = await fetchActor()To run both request concurrently one must either first start both requests, then await both:
const promisedMovie = fetchMovie()
const promisedActor = fetchActor()
const movie = await promisedMovie
const actor = await promisedActorwhich does not convey intention very well
or use explicit concurrency management functions such as Promise.all:
const [movie, actor] = Promise.all([fetchMovie(), fetchActor()])which can get convoluted quickly when there are many data dependencies.
With lacy, one can just write
const movie = lacy(fetchMovie())
const actor = lacy(fetchActor())and proceed to write business logic. The promises only need to be awaited once a value is actually needed.
React has long supported a Suspense feature that makes it possible to wrap arbitrary component trees in a suspense boundary that will show a loading state until all asynchronous logic within the subtree has resolved.
This is a really clever pattern but in most cases components need to just await (or use) all their promises before returning any markup because they need to render individual parts of the UI based on properties from the response:
async function UserAvatar() {
const user = await getUser()
return (
<div>
<img src={user.img} />
<span>{user.name}</span>
</div>
)
}To use a Suspense with this one needs to wrap the entire UserAvatar component in a boundary:
<Suspense fallback={<UserAvatarSkeleton />}>
<UserAvatar />
</Suspense>which in turn means that skeletons need to be built for entire sections that depend on the same data (and therefore need to mirror their structure):
function UserAvatarSkeleton() {
return (
<div>
<ImageSkeleton />
<TextSkeleton />
</div>
)
}Alternatively, one could use then to work with more fine-grained suspenses:
function UserAvatar() {
const user = getUser()
return (
<div>
<Suspense fallback={<ImageSkeleton />}>
<img src={user.then(user => user.image)} />
</Suspense>
<Suspense fallback={<TextSkeleton />}>
<span>{user.then(user => user.name}</span>
</Suspense>
</div>
)
}or, wrapping the suspense/skeleton pattern:
function UserAvatar() {
const user = getUser()
return (
<div>
<SuspendableImage src={user.then(user => user.image)} />
<SuspendableText>{user.then(user => user.name}</SuspendableText>
</div>
)
}but callback-style asynchronous programming is out of fashion for a reason: it is boilerplate-heavy and hard to read.
With lacy, one can write
function UserAvatar() {
const user = lacy(getUser())
return (
<div>
<SuspendableImage src={user.image} />
<SuspendableText>{user.name}</SuspendableText>
</div>
)
}which has the potential to greatly optimize data flow and loading states without compromising on readability at all.
Since the properties of a LacyPromise are all LacyPromises as well, one can easily access arbitrarily (mind the call stack size, this library in a draft state and not optimized at all) nested properties:
function getFourthActorsOtherMoviesTitle(): LacyPromise<string> {
const movie = lacy(getMovie())
return movie.actors[4].playedIn[1].title
}Lacy can also defer function calls so one can use methods of the promised objects:
function getActorNames(): LacyPromise<string[]> {
const movie = lacy(getMovie())
return movie.actors.map(actor => actor.name)
}Lacy is a library that does lazy evaluation. It also creates long laces of chained property accesses.