React Futures
Manipulate asynchronous data synchronously
Table of contents
Install
This requires you to have React's experimental build installed to enable Concurrent Mode:
#yarn
yarn add react@experimental react-dom@experimental
#npm
npm install react@experimental react-dom@experimental
To install this library:
#npm
npm i react-futures
#yarn
yarn add react-futures
Explainer
React Futures is a collection of types that allow manipulation of asynchronous data in a synchronous manner. This happens by deferring the actual data processing until after the promise resolves and suspending only when necessary. This means you don't have to worry about waiting for your fetches, just perform your usual array and object operations on the React Futures object!
For example:
; const FutureBlogs = const Blogs = { const blogs = user// fetch here // lazy // lazy const featured = blogs0 //suspend! return ...}
React Futures follows the "wait-by-necessity" principle; it defers suspension until the code examines the content of the data. Only after suspense resolves are operations like map
, slice
, and filter
applied. This simplifies data construction by hiding the data fetching logic.
When the requirements for data fetching increases, the benefits of React Futures become clearer. With React Futures the construction and consumption of the data is decoupled from the fetching of it, allowing for clearer separation of concerns.
Async/Await vs React Futures
Lets take a scenario where we want to show the active groups of a user. Here's what it would look like using async/await:
const ActiveGroups = { const activeGroups setActiveGroups = ; ; return groupslength > 0 ? <ul>groups</ul> : <div>Loading...</div>;};
And here is the same example using React Futures:
; const FutureGroups = ;const FuturePosts = ; const activeGroups = 'Tom'; const ActiveGroups = { const groups setGroups = ; return <ul> groups </ul>};
This example demonstrates several benefits of React Futures:
- With React Futures asynchronicity is transparent; a future can be used the same way that a native object or array can be used. They can also be used in other future operations, see how
FuturePosts
is used infilter
to collectactiveGroups
. - With React Futures the manipulation and construction of asynchronous data can be done completely outside render if needed. None of the construction code needs to be located inside the component.
Usage constraints
There are 3 constraints that you should be aware of and their workarounds.
1. Getters are only allowed in render
Due to how Concurrent Mode works, suspense operations (i.e. getters) are only allowed in the render function. For example:
const blogs = const first = blogs0 // Error: suspense not allowed outside render const App = { const first = blogs0 // suspend! return ...}
Keep in mind that on
handlers are not considered a part of render, even though they may be located within the render function.
To work around this, React Futures provides utilities that defer evaluation of getters until suspense (see Suspense operations outside render and Using React Futures with third party libraries). In brief, you should wrap getters performed outside render in lazyArray
or lazyObject
.
2. No mutable calls inside of render
Operations that mutate a future array or object, like Object.assign
, are not allowed within render and should be replaced with their immutable equivalents. To alleviate this constraint, all future object constructor static methods have been made immutable. For example, <Future Class>.assign
is an immutable, deferred version of Object.assign
:
const FutureUser = const dave = 'Dave' const newDave = Object // okayconst newDave2 = FutureUser // okay const App = { const newDave = Object // Error!! const newDave2 = FutureUser // okay return ...}
3. Global ban on specific functions
Some operations are both mutable and are getters, like array.push
and array.unshift
. These operations are prohibited globally.
For a complete reference of constraints, see the Caveats section.
Example snippets
Object iteration
Object iteration with native types is normally done with Object.entries
and Object.fromEntries
, but with a future this would suspend since Object.entries
is a getter. You should instead use the new <Future Class>.entries
and <Future Class>.fromEntries
, which will defer evaluation until necessary. For example:
;const FutureUser = ; const user = ; const uppercaseUserEntries = FutureUser //lazy ; const uppercaseUser = FutureUser; // lazy
All future object static methods are deferred and immutable variants of Object
static methods, so they can used both in and out of render.
Suspense operations outside render
Sometimes it's useful to access properties on a future or perform a suspense operation outside of render. An example of this is transforming properties on a future. However, accessing a property will throw an error if done outside render.
const dave = "Dave"; daveprops = daveprops // Error: suspense operations not allowed outside render ;
To achieve this, use lazyArray
or lazyObject
to suspend evaluation.
; const FutureUser = ;const dave = "Dave"; daveprops = //=> future array ; // lazy
The above snippet suspends evaluation of the dave.props
getter until a suspense operation is performed on dave.props
inside render. lazyArray
returns a future array and an lazyObject
returns a future object.
Sometimes performing a suspense operation is inside an on
handler is desired, but suspense operations are illegal in on
handlers as well.
const dave = "Dave"; const App = { const user setUser = ; return <> <input type="text" onChange= { // spread operator on `user` errors since spreading is a suspense operation ; } /> </> ;};
To accomplish this, we can use the lazyObject
from above.
// Using `lazyObject`const dave = 'Dave'; const App = { const user setUser = return <> <input type="text" onChange= { const newUser = ) //=> future object } /> </>}
Alternatively, we can use the getRaw
function to force a suspend and get the raw object:
// Using `getRaw`; const dave = 'Dave'; const App = { const user setUser = // suspends here and gets raw value return <> <input type="text" onChange= { const newUser = ...user name: etargetvalue // can spread, since it's just a normal object } /> </>}
Using with third party libraries (ramda, lodash, etc.)
Third party libraries that inspect the contents of input parameters will suspend if passed in a future. To prevent this use lazyArray
and lazyObject
. These methods lazily evaluate array and object returning functions respectively.
Lets take a look at an example using lodash's _.cloneDeep
. If you pass a future in the function, it would suspend since _.cloneDeep
iterates through the properties of the future.
const FutureUser = ; const dave = 'Dave'; const daveTwin = _; // Error: can not suspend outside render
To allow this, use the lazyObject
to defer evaluation of _.cloneDeep
const FutureUser = ; const dave = 'Dave'; const daveTwin = //=> future object // continue iterating as you would a futureconst result = FutureUser
lazyObject
defers the evaluation of the object returning operation until a suspense operation takes place.
lazyArray
works the same way for arrays. Here's an example using ramda's zip
function:
const FutureFriends = const FutureGroups = const friends groups = ; const friendsAndGroups = //=> future array
To defer function composition, you can use ramda's pipeWith
or composeWith
functions and wrap callbacks in lazyObject
and lazyArray
:
;; const FutureFriends = const pipeFuture = const lazyInternationalFriendsByGrade = const internationalFriendsByGrade = // => future array
Cache invalidation
React Futures use values passed into a future constructor as keys for an in-memory cache, so multiple instantiations with the same constructor will pull from the cache
const dave = "Dave"; // fetchesconst jen = "Jen"; // fetches const App = { const dave = "Dave"; // pulls from cache const jen = "Jen"; // pulls from cache const harold = "Harold"; // fetches};
Since caching is performed using LRU, invalidation is done automatically after the cache reaches a certain size and the key hasn't been used in a while.
To manually invalidate a key, you can use the static method invalidate
on the future constructor.
const dave = "Dave"; // fetches; const App = { let dave = "Dave"; // pulls from cache FutureUser; // deletes 'Dave' from cache dave = "Dave"; // fetches};
Sometimes it's useful to clear the entire cache, like on a page refresh. This can be accomplished using the static method reset
on the future constructor
const dave = "Dave";const jen = "Jen";const harold = "Harold";const App = { ;};
Fetching on component mount
Sometimes it's desirable to fetch whenever a component is mounted, similar to how you would in the good old days with fetch and componentDidMount. To achieve this with futures, use useEffect
to invalidate the cache on unmount.
const useUser = { const user = name; // fetches on first render return user} const App = { const user = ; return ...}
With classes the invalidation can be placed inside componentWillUnmount
Component { const user = 'Dave' // fetches this } { FutureUser // invalidates cache } { return ... }
Prefetching
One of the focuses of suspense and concurrent mode is 'prefetching' which is fetching data way before we need it. This is great for performance as it shortens the percieved wait time of fetched data.
There is no explicit "prefetch" api in React Futures, fetching occurs whenever a future is instantiated. To prefetch simply instantiate the future outside of render or within a parent component.
const user = 'Dave' // instantiating outside of render will prefetch 'user' as file parses const App = { const friends = 'Dave' // prefetch in parent component const family = 'Dave' // prefetch in parent component const currentPage = return <> currentPage === 'family' ? <Family family=family /> : // 'family' suspended by <Family /> currentPage === 'friends' ? <Friends friends=friends> : null // 'friends' suspended by <Friends /> </>}
Logging
console.log
with a future will log a proxy, which is probably not what you want. To log the contents of the future use either toPromise
or getRaw
.
;... const App = { console; return <div></div>}
If future has any nested futures, those will not be visible with toPromise
or getRaw
. Here is an example of how you could implement getRawDeep for deep logging.
; const getRawDeep = { if const raw = ; return ; ifArray return future; else iftypeof future === 'object' && future !== null return Object return future;}
Using with graphql
Coming soon...
Caveats
Operation constraints
As a rule of thumb, mutable operations are constrained to outside render and suspense operations are constrained to inside render. For suspense operation workarounds see Suspense operations outside render and Using React Futures with third party libraries.
Consider moving mutable operations outside render or using an immutable variant instead. All future object constructor static method have been made immutable and lazy.
Certain operations are forbidden globally since they are both mutable and they inspect the contents of the array. array.push
is mutable, for example, prohibiting it from being used in render, and it synchronously returns the length of the array, prohibiting it from being used outside render.
Other operations are tbd since it is uncertain what the use cases for these methods are vs. Object
static methods. Depending on how they can be useful, the implementation can vary significantly. Click below for a complete reference of constraints
Complete restriction reference
FutureObjectConstructor represents the class returned by `futureObject`
Suspend methods: disallowed outside render
futureArray.indexOf()futureArray.includes()
futureArray.join()
futureArray.lastIndexOf()
futureArray.toString()
futureArray.toLocaleString()
futureArray.find()
futureArray.every()
futureArray.some()
futureArray.findIndex()
futureArray.reduce()
futureArray.reduceRight()
Object.assign(object, futureObject)
Object.getOwnPropertyDescriptors(future, ...rest)
Object.getOwnPropertyNames(future)
Object.getOwnPropertySymbols(future)
Object.isExtensible(future)
Object.isFrozen(future)
Object.isSealed(future)
Object.keys(future)
Object.entries(future)
Object.values(future)
Object.getPrototypeOf(future)
FutureObjectConstructor.isExtensible(future)
FutureObjectConstructor.isFrozen(future)
FutureObjectConstructor.isSealed(future)
Mutable methods: disallowed inside render
futureArray.splice()futureArray.copyWithin()
futureArray.sort()
futureArray.unshift()
futureArray.reverse()
futureArray.fill()
Object.defineProperties(future)
Object.defineProperty(future)
Object.setPrototypeOf(future)
Immutable methods: allowed anywhere
futureArray.concat()futureArray.filter()
futureArray.slice()
futureArray.map()
futureArray.flat()
futureArray.flatMap()
FutueObjectConstructor.getOwnPropertyDescriptor(future)
FutueObjectConstructor.getOwnPropertyDescriptors(future)
FutueObjectConstructor.getOwnPropertyNames(future)
FutueObjectConstructor.getOwnPropertySymbols(future)
FutueObjectConstructor.getPrototypeOf(future)
FutueObjectConstructor.keys(future)
FutueObjectConstructor.entries(future)
FutueObjectConstructor.values(future)
FutureObjectConstructor.assign(future, ...rest)
FutureObjectConstructor.assign(obj, future, ...rest)
FutureObjectConstructor.seal(future)
FutureObjectConstructor.preventExtensions(future)
FutureObjectConstructor.defineProperties(future, descriptors)
FutureObjectConstructor.defineProperty(future, prop, descriptor)
FutureObjectConstructor.freeze(future)
FutureObjectConstructor.setPrototypeOf(future)
Invalid methods: disallowed globally (if you feel that these shouldn't error, please submit an issue explaining your use case)
FutureObjectConstructor.create # not sure how this should differ from behavior of Object.createFutureObjectConstructor.is #should this compare future wrapper or raw value? If future wrapper, what would be the difference between this and `Object.is`?
futureArray.push # both mutable and requires sync get of `length`
futureArray.pop # both mutable and requires sync get of the popped value
futureArray.shift # both mutable and requires sync get of `length`
futureArray.unshift # both mutable and requires sync get of unshifted value
delete futureObject # both mutable and requires knowledge of object property descriptors, since it returns true or false depending on whether operation succeeded
Object.preventExtensions(future) # Causes problems in proxy
futureArray.forEach() # Requires react futures to resolve a future without suspense, which is not yet implemented. Not even sure this is a good since deferred side-effects can cause unexpected behavior, plus what benefit would this have over a for loop in `lazyArray` or 'lazyObject'?
lazyObject and lazyArray in reassignment
Using lazyObject
and lazyArray
to perform a reassignment inside a loop can lead to an unexpected error. This is because the right hand side does not evaluate first since lazyObject
and lazyArray
is deferred.
const arr = ;forconst futureItem of items arr = // leads to getter loop of `arr` on suspense
to avoid this bug, encapsulate the whole block in lazyObject
/lazyArray
const arr =