When the redux-sagas are to much, and the redux-thunk is not enough.
- Redux-tx is for the redux, so you will need it.
- Internally it is using using zone.js.
- And redux-tx is plays best with redux-thunk.
The main pompous of this lib is to simplify the way of working with the async action. The one think what it is giving for you is possibility for encapsulated multiple async state mutations in one time state drop. The second think is possibility for async actions cancellation.
Redux-tx is a small library which allows you to execute actions in transaction. If some set of actions will be dispatched within transaction, the global state will be not updated until the transaction will be not committed. All of such actions will modify the copy of state. Such transaction can be simple cancelled, and then your state will be not touched. If transaction will failed (the exception will be throw within transaction) the transaction will rollback automatically. Transactions supports also timeouts, so if transaction will take more than some time (default 1min), transaction will throw exception, and will be rollback.
Redux-tx is fully supporting redux dev tools.
For solving the race problem we are using Optimistic Lock solution on each primitive value in the state. This mean that you are able to run multiple transactions in parallel, or transaction and synchronous actions as far as you do not mutate same values in state.
To install the stable version:
npm install --save redux-tx
To use transactions we have to configure storage by wrapping reducer by transactionReducer
and add transactionMiddleware
to the
middleware:
import {applyMiddleware, combineReducers, createStore} from 'redux';
import {transactionReducer, transactionMiddleware} from 'redux-tx';
import thunk from 'redux-thunk';
const store = createStore(
// we can use combined reducer, but we can use also regular one
// but important is that we have to wrap top level reducer
transactionReducer(combineReducers({
search: myReducer
})),
applyMiddleware(thunk, transactionMiddleware)
);
To run the transaction you simple have to dispatch your action in transaction closure:
import {transaction} from 'redux-tx';
// transaction function returns augmented Promise object
let tx = transaction(async () => {
await store.dispatch(someAsyncThunkAction());
})
tx.then(() => {
// transaction was commited, the state is modified.
console.log(tx.state); // => 'COMMITED'
});
tx.catch(() => {
// transaction throwed error, the state is not modified.
console.log(tx.state); // => 'REJECTED'
});
To cancel transaction you are have to call cancel
function on transaction Promise
import {transaction} from 'redux-tx';
// transaction function returns augmented Promise object
let tx = transaction(async () => {
await store.dispatch(someAsyncThunkAction());
})
tx.then(() => {
// then will be called even on cancelled transaction but you can check the state of transaction
console.log(tx.state); // => 'CANCELLED'
});
tx.cancel();
- When you are calling
transaction
function redux-tx is- creating new zone
- dispatching
StartTransactionAction
action - reducer for
StartTransactionAction
is creating new state branch with the copy of current state
- When you dispatching action within the transaction
- the action is marked by middleware with the transaction identifier (taken from zone)
- your reducer is getting the state from brunch instead of root reference
- if you mutate state, all mutations are going to the branch instead of root reference
- After finish of transaction if any exception was not thrown and transaction was not cancelled
CommitTransactionAction
action is dispatched- reducer is merging the state from transaction in to root state reference,
merge is comparing current state with the state which application had on transaction start (beforeState),
to distinguish there was Race occurs
- if race occurs the exception will be thrown, transaction promise will be rejected with exception
- if there is no race, transaction promise will be resolved, and the root state reference is mutated
- If there was exception thrown within the transaction
RejectTransactionAction
action is dispatched- reducer is removing the branch from state
- transaction promise will be rejected with exception
- If there timeout occurs within the transaction
TimeoutTransactionAction
action is dispatched- reducer is removing the branch from state
- transaction promise will be rejected with transaction timeout exception
- If during the transaction processing there will be a cancellation
CancelTransactionAction
action is dispatched- reducer is removing the branch from state
- transaction promise will be resolved
Live example (please use dev tool)
/**
* Transaction phase constant values.
*/
type TransactionPhase = 'CANCELLED' | 'REJECTED' | 'COMMITED' | 'TIMEOUTED' | 'PENDING';
/**
* Promise with possibility to cancel with state flags.
*/
interface TransactionPromise extends Promise<void> {
/**
* Current state of transaction.
*/
state: TransactionPhase;
/**
* Break the transaction.
*/
cancel(): void;
}
/**
* Run code in transaction.
* @param dispatch redux dispatch function
* @param tx the transaction code
* @param timeout optional timeout for transaction, if not provided, the global one will be used, the default is 1 min
*/
function transaction(dispatch: Dispatch<any>, tx: () => void | Promise<void>, timeout?: number): TransactionPromise;
/**
* Run code in transaction.
* @param name name of transaction
* @param dispatch redux dispatch function
* @param tx the transaction code
* @param timeout optional timeout for transaction, if not provided, the global one will be used, the default is 1 min
*/
function transaction(name: string, dispatch: Dispatch<any>, tx: () => void | Promise<void>, timeout?: number): TransactionPromise;
/**
* Sets default timeout for all transactions.
* @param timeout
*/
function setTransactionTimeout(timeout: number): void;
/**
* Action transactions enhance middleware.
* This middleware is responsible for mark action with transaction id, and reject actions which are targeting not existing.
*/
function transactionMiddleware<S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
/**
* Create the reducer which branching the state between the transactions.
* @param reducer wrapped application reducer.
*/
function transactionReducer<S>(reducer: Reducer<S>): Reducer<S>;