Actions and reducers
In Async Redux, an action is any class you create that extends ReduxAction<State>
.
The ReduxAction
is a built-in class provided by Async Redux,
and State
is the type you defined for your application state.
For example, this is how you can declare an Increment
action,
that could be used in a counter application:
import { ReduxAction } from "async-redux-react";
import { State } from 'State';
class Increment extends ReduxAction<State> { }
The reducer
All your actions must implement a function called reduce()
.
Your IDE will show a compile-time error if you forget to implement it.
class Increment extends ReduxAction<State> {
reduce() {
// ...
}
}
The reduce()
function is called a reducer.
We'll soon see that when actions are "dispatched", its reducer will be called to calculate state changes in your app.
To achieve this, the reducer has direct access to the current application state
through this.state
, and then it must return a new state. For example:
class Increment extends ReduxAction<State> {
reduce() {
// The reducer has access to the current state
return new State(this.state.counter + 1); // Returns a new state
}
}
In the code that dispatches an action, you can use your IDE to click the action name and go to where the action is defined. There, you'll find the reducer for that action, which explains what happens when the action is dispatched.
In other words, the action and its reducer are part of the same data structure, keeping your code organized.
Base action
Having to write extends ReduxAction<State>
in every action definition can be cumbersome.
In all the code I show in this documentation, you'll see I usually write extend Action
instead of extend ReduxAction<State>
.
This is because I'm assuming you have previously defined your own abstract base action class
called simply Action
, that itself extends ReduxAction<State>
. Then, you may have all your
actions extend this Action
class instead.
This is how you can define the Action
class in your own code:
import { ReduxAction } from 'async-redux-react';
import { State } from 'State';
export abstract class Action extends ReduxAction<State> { }
And then:
import { Action } from './Action';
class Increment extends Action {
// ...
}
Later, we'll see that the base action is also a good place to put common logic.
Actions can have parameters
The above Increment
action is simple and doesn't take any parameters.
But actions can take any number of parameters, just like functions.
Consider the following Add
action:
class Add extends Action {
constructor(readonly value: number) { super(); }
reduce() {
return this.state.add(this.value);
}
}
In the above example, the Add
action takes a value
parameter in its constructor.
When you dispatch the Add
action, you pass the value as a parameter:
dispatch(new Add(5));
Note the reducer has direct access to the value
parameter through this.value
.
Actions can do asynchronous work
The simplest type of action is synchronous, meaning it doesn't involve any asynchronous operation.
We can know an action is sync by looking at its reducer, which is declared with reduce()
.
However, action can download information from the internet, or do any other async work.
To make an action async, declared it with async reduce()
and then returns a Promise
.
Also, instead of returning the new state directly, you should return a function that will change the state.
For example, consider the following AddRandomText
action,
that fetches a random text from the internet and adds it to the state:
class AddRandomText extends Action {
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}));
}
}
If you want to understand the above code in terms of traditional Redux patterns,
the beginning of the reduce
method is the equivalent of a middleware,
and the return function (state) => state.copy({text: text}))
is the equivalent of
a traditional pure reducer.
It's still Redux, just written in a way that is easy and boilerplate-free. No need for Thunks or Sagas.
Actions can throw errors
If something bad happens, your action can simply throw an error. In this case, the state will not change.
Let's modify the previous AddRandomText
action to throw an error if the fetch fails:
import { UserException } from "async-redux-react";
class AddRandomText extends Action {
async reduce() {
let response = await fetch("https://dummyjson.com/todos/random/1");
if (!response.ok) throw new UserException("Failed to load.");
let jsonResponse = await response.json();
let text = jsonResponse[0].todo;
return (state) => state.copy({text: text}));
}
}
Notes:
-
Any errors thrown by actions are caught globally and can be handled in a central place. More on that, later.
-
Actions can throw any type of errors. However, if they throw a
UserException
(provided by Async Redux), a dialog or other UI will open automatically, showing the error message to the user.
Next, let's see how and why you can have actions that don't modify the state.