We are pleased to announce the release of Recoil 0.4 with configurable selector caches, improved API for transactions with multiple atoms, and other optimizations and fixes.
Configurable selector caches
The new cachePolicy_UNSTABLE
property in selectors and selector families allows you to configure the caching behavior of a selector's internal cache. This property can be useful for reducing memory in applications that have a large number of selectors or selectors that have a large number of changing dependencies.
Below is an example of how you might use this new property:
const clockState = selector({
key: 'clockState',
get: ({get}) => {
const hour = get(hourState);
const minute = get(minuteState);
const second = get(secondState); // will re-run every second
return `${hour}:${minute}:${second}`;
},
cachePolicy_UNSTABLE: {
// Only store the most recent set of dependencies and their values
eviction: 'most-recent',
},
});
In the example above, clockState
recalculates every second, adding a new set of dependency values to the internal cache, which may lead to a memory issue over time as the internal cache grows indefinitely. Using the most-recent
eviction policy, the internal selector cache will only retain the most recent set of dependencies and their values, along with the actual selector value based on those dependencies, thus solving the memory issue.
Current eviction options are:
lru
- evicts the least-recently-used value from the cache when the size exceedsmaxSize
.most-recent
- retains only the most recent value.keep-all
(default) - keeps all entries in the cache and does not evict.
NOTE: The default eviction policy (currently
keep-all
) may change in the future.
Transactions with multiple atoms
Introducing an improved API for updating multiple atoms together as a single transaction. The new useRecoilTransaction_UNSTABLE()
hook is easier, more efficient, and safer than before. This new hook should eventually replace most uses of useRecoilCallback()
, however this release is only an initial implementation with certain limitations that will be addressed in future releases.
Example
Suppose we have two atoms, positionState
and headingState
, and we'd like to update them together as part of a single action, where the new value of positionState
is a function of both the current value of positionState
and headingState
. You can accomplish this with a transaction, which must be a pure function without side-effects:
const goForward = useRecoilTransaction_UNSTABLE(({get, set}) => (distance) => {
const heading = get(headingState);
const position = get(positionState);
set(positionAtom, {
x: position.x + cos(heading) * distance,
y: position.y + sin(heading) * distance,
});
});
Then you can execute the transaction by just calling goForward(distance)
in an event handler. This will update state based on the current values, not the state when the components rendered. You can also read the values of previous writes during a transaction. Because no other updates will be committed while the updater is executing, you will see a consistent store of state.
the previous approach using useRecoilCallback()
might have looked like the following:
const goForward = useRecoilCallback(({snapshot, gotoSnapshot}) => (distance) => {
const mutatedSnapshot = snapshot.map(({get, set}) => {
const heading = get(headingState);
const position = get(positionState);
set(positionState, {
x: position.x + cos(heading) * distance,
y: position.y + sin(heading) * distance,
});
});
gotoSnapshot(mutatedSnapshot);
});
This has the following drawbacks:
- There is performance overhead for managing the full generality of snapshots.
- There is more opportunity for bugs: The snapshot might be retained and used in the future. Since a snapshot contains the complete set of Recoil state, not just a changeset, that could accidentally rewind changes that occurred between creating and committing the snapshot.
Reducer Example
You can also use this hook to create a reducer pattern of executing actions over multiple atoms:
const reducer = useRecoilTransaction_UNSTABLE(({get, set}) => action => {
switch(action.type) {
case 'goForward':
const heading = get(headingState);
set(positionState, position => {
x: position.x + cos(heading) * action.distance,
y: position.y + sin(heading) * action.distance,
});
break;
case 'turn':
set(headingState, action.heading);
break;
}
});
Fixes and Optimizations
- Fix TypeScript typing for
selectorFamily()
,getCallback()
,useGetRecoilValueInfo()
, andSnapshot#getNodes()
(#1060, #1116, #1123) - Allow mutable values in selectors (enabled via the
dangerouslyAllowMutability
selector option) to be used withwaitFor*()
helpers such aswaitForAll()
(#1074, #1096) - Atom Effects fixes:
- Fix
onSet()
handler to get the proper new value when an atom is reset or has an async default Promise that resolves (#1059, #1050, #738) (This is a slightly breaking change because the actual new value will be provided to the handler instead of aDefaultValue
placeholder) - Fix support for multiple Atom Effects cleanup handlers (#1125)
- Fix selector subscriptions when atoms with effects are initialized via a
Snapshot
(#1135, #1107)
- Fix
- Optimization for async selectors when dependencies resolve to cached values (#1037)
- Remove unnecessary warning message (#1034, #1062)