Action features
You can add features to your actions, to accomplish common tasks:
nonReentrant
To prevent an action from being dispatched while it's already running,
add nonReentrant = true
to your action.
class LoadText extends Action {
nonReentrant = true;
reduce() { ... }
}
retry
To retry an action a few times with exponential backoff, if it fails,
add the retry
property to your action class.
class LoadText extends Action {
retry = {on: true}
reduce() { ... }
}
The retry parameters are:
- Initial Delay: The delay before the first retry attempt.
- Multiplier: The factor by which the delay increases for each subsequent retry.
- Maximum Retries: The maximum number of retries before giving up.
- Maximum Delay: The maximum delay between retries to avoid excessively long wait times.
And their default values are:
initialDelay
is350
milliseconds.multiplier
is2
, which means the default delays are: 350 millis, 700 millis, and 1.4 seg.maxRetries
is3
, meaning it will try a total of 4 times.maxDelay
is5000
milliseconds (which means 5 seconds).
You can change one or more of the default values.
class LoadText extends Action {
retry = {
initialDelay: 350, // Millisecond delay before the first attempt
maxRetries: 3, // Number of retries before giving up
multiplier: 2, // Delay increase factor for each retry
maxDelay: 5000, // Max millisecond delay between retries
}
reduce() { ... }
}
If you want to retry unlimited times, make maxRetries
equal to: -1
:
class LoadText extends Action {
retry = {maxRetries: -1};
}
Notes:
-
If you
await dispatchAndWait(action)
and the action uses unlimited retries, it may never finish if it keeps failing. So, be careful when using it. -
If the
before
method throws an error, the retry will NOT happen. -
The retry delay only starts after the reducer finishes executing. For example, if the reducer takes 1 second to fail, and the retry delay is 350 millis, the first retry will happen 1.35 seconds after the first reducer started.
-
When the action finally fails, the last error will be rethrown, and the previous ones will be ignored.
-
For most actions that use
retry
, consider also making them non-Reentrant to avoid multiple instances of the same action running at the same time:class MyAction extends ReduxAction<State> {
retry = {on: true}
nonReentrant = true;
} -
Keep in mind that all actions using the
retry
feature will become asynchronous, even if the original action was synchronous. -
If necessary, you can know the current attempt number by using
this.attempts
.
debounce
To limit how often an action occurs in response to rapid inputs,
add something like debounce = 300
to your action class,
where 300
is the number of milliseconds.
For example, when a user types in a search bar, debouncing ensures that not every keystroke triggers a server request. Instead, it waits until the user pauses typing before acting.
class SearchText extends Action {
constructor(public searchTerm: string) { super(); }
debounce = 300 // Milliseconds
async reduce() {
let result = await loadJson('https://example.com/?q=', searchTerm);
return (state) => state.copy({searchResult: result});
}
}
Important: this feature is still in development. It should be available soon.
throttle
To prevent an action from running too frequently,
add something like throttle = 5000
to your action class,
where 5000
means 5 seconds.
After the action runs it's considered fresh, and it won't run again for a set period of time, even if you dispatch it. After this period ends, the action is considered stale and is ready to run again.
class LoadPrices extends Action {
throttle = 5000 // Milliseconds
async reduce() {
let result = await loadJson('https://example.com/prices');
return (state) => state.copy({prices: result});
}
}
Important: this feature is still in development. It should be available soon.
ignoreOld
If some action that performs a slow async process may be dispatched multiple times in a rapid sequence, you may want to ignore its result if a newer action of the same type was already dispatched. This avoids race conditions where the result of an older action overwrites the result of a newer one.
In Async Redux this problem can be easily solved by adding ignoreOld = true
to
your action class:
class LoadText extends Action {
ignoreOld = true
async reduce() {
let response = await fetch("https://dummyjson.com/todos/random/1");
let jsonResponse = await response.json();
let text = jsonResponse[0].todo;
return (state) => state.copy(text: text));
}
}
In the above example, if the action gets called twice or more before the first one finishes, only the last one will be considered, and the previous ones will be ignored.
In more detail, the returned state from the reducer will only be applied if the action is the most recent one dispatched. If it's not, the state will be discarded.
Note that we are not aborting the requests, but simply ignoring the reducer result. This is fine for most cases.
If we actually want to abort the requests, we need to use
an AbortController
by calling this.getAbortController()
in the action, and then using that controller to
abort the request:
class LoadText extends Action {
ignoreOld = true
async reduce() {
let abortController = this.getAbortController(); // Here!
let response = await fetch("https://dummyjson.com/todos/random/1", {
signal: abortController.signal, // Here!
});
let jsonResponse = await response.json();
let text = jsonResponse[0].todo;
return (state) => state.copy(text: text));
}
}
Async Redux will automatically abort the previous requests by calling abortController.abort()
when a new action is dispatched.
When this happens, the fetch()
promise will reject with an AbortError
,
but this error is caught by Async Redux and ignored, so you don't need to worry about it.
If instead of fetch
you use Axios, it also works:
let response = await axios.get('https://dummyjson.com/todos/random/1', {
signal: abortController.signal
});
Note: The ignoreOld
feature is not compatible with the nonReentrant feature,
because they are opposite concepts. While nonReentrant
aborts the newer actions
if an older one is running, ignoreOld
aborts the older actions if a newer one is running.
If you try to use both at the same time, an error will be thrown.
Important: this feature is still in development. It should be available soon.
checkInternet
Adding checkInternet = { dialog: true }
to your action ensures it only runs with internet,
otherwise an error dialog prompts users to check their connection:
class LoadPrices extends Action {
checkInternet = { dialog: true }
async reduce() { ... }
}
Use checkInternet = { dialog: false }
if you don't want to open a dialog.
Instead, you can display some information in your widgets:
function MyComponent() {
const isFailed = useIsFailed(LoadPrices);
return (
<div>
{isFailed ? <p>No Internet connection</p> : null}
</div>
);
};
Important: this feature is still in development. It should be available soon.
optimisticUpdate
To provide instant feedback on actions that save information to the server, this feature immediately applies state changes as if they were already successful, before confirming with the server.
If the server update fails, the change is rolled back and, optionally, a notification can inform the user of the issue.
class SaveName extends Action {
optimisticUpdate = { ... }
async reduce() { ... }
}
Important: this feature is still in development. It should be available soon.