useRecoilTransaction_UNSTABLE(callback, deps)
Create a transaction callback which can be used to atomically update multiple atoms in a safe, easy, and efficient way. Provide a callback for the transaction as a pure function which can get()
or set()
multiple atoms. A transaction is similar to the "updater" form of setting Recoil state, but can operate over multiple atoms. Writes are visible to subsequent reads from within the same transaction.
In addition to transactions, this hook is also useful to:
- Implement reducer patterns to perform actions on multiple atoms.
- Dynamically updating an atom where we may not know at render-time which atom or selector we will want to update, so we can't use
useSetRecoilState()
. - Pre-fetching data before rendering.
interface TransactionInterface {
get: <T>(RecoilValue<T>) => T;
set: <T>(RecoilState<T>, (T => T) | T) => void;
reset: <T>(RecoilState<T>) => void;
}
function useRecoilTransaction_UNSTABLE<Args>(
callback: TransactionInterface => (...Args) => void,
deps?: $ReadOnlyArray<mixed>,
): (...Args) => void
callback
- User callback function with a wrapper function that provides the transaction interface. This function must be pure without any side-effects.deps
- An optional set of dependencies for memoizing the callback. LikeuseCallback()
, the produced transaction callback will not be memoized by default and will produce a new function with each render. You can pass an empty array to always return the same function instance. If you pass values in thedeps
array, a new function will be used if the reference equality of any dep changes. Those values can then be used from within the body of your callback without getting stale. (SeeuseCallback
) You can update eslint to help ensure this is used correctly.
Transaction Interface:
get
- Get the current value for the requested Recoil state, reflecting any writes performed earlier in the transaction. This currently only supports synchronous atoms.set
- Set the value of an atom. You may either provide the new value directly or an updater function that returns the new value and takes the current value as a parameter. The current value represents all other pending state changes to date in the current transaction.reset
- Reset the value of an atom to its default.
Transaction 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
.
const goForward = useRecoilTransaction_UNSTABLE(({get, set}) => (distance) => {
const heading = get(headingState);
const position = get(positionState);
set(positionState, {
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.
const moveInAnL = useRecoilTransaction_UNSTABLE(({get, set}) => () => {
// Move Forward 1
const heading = get(headingState);
const position = get(positionState);
set(positionState, {
x: position.x + cos(heading),
y: position.y + sin(heading),
});
// Turn Right
set(headingState, heading => heading + 90);
// Move Forward 1
const newHeading = get(headingState);
const newPosition = get(positionState);
set(positionState, {
x: newPosition.x + cos(newHeading),
y: newPosition.y + sin(newHeading),
});
});
Reducer Example
This hook is also useful for implementing reducer patterns to execute 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;
}
});
Current Limitations and Future Vision
- Transactions currently only support atoms, not yet selectors. This support can be added in the future.
- Atoms with default values that are selectors are also not yet supported.
- Atoms that are read must have a synchronous value. If it is in an error state or an asynchronous pending state, then the transaction will throw an error. It would be possible to support pending dependencies by aborting the transaction if a dependency is pending and then re-starting the transaction when it is available. This is consistent with how the selector
get()
is implemented. - Transactions do not have a return value. If we want to have some notification a transaction completes, or use transactions to request slow data, or to request data from event handlers, then we could have a transaction return a
Promise
to a return value. - Transactions must be synchronous. There is a proposal to allow asynchronous transactions. The user could provide an
async
transaction callback function which could useawait
. The atomic update of all sets would not be applied, however, until thePromise
returned by the transaction is fully resolved. - Transactions must not have any side-effects. If you require side-effects, then use
useRecoilCallback()
instead.