Async Actions
Introduction
In this section, we will learn how to use GX
to manage asynchronous actions in your application.
Async Actions
are available since the version 1.4.0
.
What is an asynchronous action ?
An asynchronous action is an action that is not executed immediately, but after a certain time. For example, when you want to fetch data from an API, you have to wait for the response of the API before updating the state of your application.
How to create asynchronous actions ?
To create asynchronous actions, you have to define the asyncActions
property of the createSignal
function and then define your asynchronous actions inside it by using a builder
in order to build your async actions
.
That's the global idea, we'll see exactly how to do that step by step and you'll see that it's not hard at all. 😊
Step 1: Create a signal first
Firstly, let's consider the situation where we want to fetch movies from an API. We will create a signal called moviesSignal
that will contain the list of movies.
import { createSignal } from "@dilane3/gx";
const moviesSignal = createSignal({
name: "movies",
state: {
movies: [],
loading: false,
error: null,
},
});
export default moviesSignal;
As you already know, the createSignal
function takes an object as a parameter. This object contains the following required properties:
name
: The name of the signal.state
: The initial state of the signal.
We decided to define the initial state of the signal as follows:
{
movies: [],
loading: false,
error: null,
}
Because we want to store the list of movies in the movies
property, we want to store the loading state in the loading
property and we want to store the error if it occurs in the error
property.
Don't forget to export your signal at the end of the file and to create a store in order to use it in your application.
If you don't know how to do that, you can read the Create Store section.
Step 2: Create the asynchronous action
Now that we have created our signal, we can create our asynchronous action. To do this, we will use the function createAsyncAction
which takes an asynchronous handler method as a parameter. This handler method will be called when the asynchronous action is executed.
import { createSignal, createAsyncAction } from "@dilane3/gx";
// Async handler method
const fetchMovies = createAsyncAction(async () => {
// fetch movies list from themoviedb API
const response = await fetch(
"https://api.themoviedb.org/3/account/{account_id}/lists"
);
const movies = await response.json();
return movies;
});
const moviesSignal = createSignal({
name: "movies",
state: {
movies: [],
loading: false,
error: null,
},
});
export default moviesSignal;
As you can see, we have defined the fetchMovies
asynchronous action by using the createAsyncAction
function.
Note that we haven't used try/catch to catch errors because GX
will do it for us. We will see how to handle errors later.
Step 3: Define the asynchronous actions property of the signal
Now that we have created our asynchronous action, we can define it in the asyncActions
property of the signal.
import { createSignal, createAsyncAction } from "@dilane3/gx";
// Async handler method
const fetchMovies = createAsyncAction(async () => {
// fetch movies list from themoviedb API
const response = await fetch("https://api.themoviedb.org/3/account/{account_id}/lists");
const movies = await response.json();
return movies;
})
const moviesSignal = createSignal({
name: "movies",
state: {
movies: [],
loading: false,
error: null,
},
asyncActions: (builder) => ({
fetchMovies: // Use the builder to build your async action by defining all cases of the async action
})
});
The asyncActions
property takes a function as a parameter and this function takes a builder
as a parameter. The builder
is used to build your asynchronous action by defining all cases of the asynchronous action.
Step 4: Define cases of the asynchronous action
Now that we have defined the asyncActions
property of the signal, we can define the cases of the asynchronous action by using the builder
that we have received as a parameter.
import { createSignal, createAsyncAction } from "@dilane3/gx";
// Async handler method
const fetchMovies = createAsyncAction(async () => {
// fetch movies list from themoviedb API
const response = await fetch(
"https://api.themoviedb.org/3/account/{account_id}/lists"
);
const movies = await response.json();
return movies;
});
const moviesSignal = createSignal({
name: "movies",
state: {
movies: [],
loading: false,
error: null,
},
asyncActions: (builder) => ({
fetchMovies: builder
.use(fetchMovies)
.case(fetchMovies.pending, (state) => {
state.loading = true;
return state;
})
.case(fetchMovies.fulfilled, (state, data) => {
state.movies = data;
state.loading = false;
return state;
})
.case(fetchMovies.rejected, (state, error) => {
state.error = error;
state.loading = false;
return state;
}),
}),
});
Okay okay let's stop here for a moment. I know that you are a little bit lost but don't worry, I will explain everything to you. 😊
As you can see, we have used the builder
to build our asynchronous action by defining all cases of the asynchronous action.
The builder
has only one method called use
. This method is used to define the asynchronous action that we want to use. It takes the asynchronous action as a parameter and return an instance of a BuilderCase
class.
The BuilderCase
class is used to define the cases of the asynchronous action. It has a method called case
that takes asynchronous action status
as a first parameter and a callback function that takes the state and the data as parameters and returns the new state.
The callback function is called when the asynchronous action status is equal to the status passed as a parameter. And this callback function has to always return the new state in order to modify the state of the signal.
When the asynchronous action status is equal to PENDING
, we don't have access to any payload. That's why we have defined the case
of the PENDING
status as follows:
case(fetchMovies.pending, (state) => {
state.loading = true;
return state;
})
With only a state as a parameter and we have returned the new state by setting the loading
property to true
.
Rather than using the case
method to define the cases of the asynchronous action by setting the status by yourself, you can use methods like onPending
, onFulfilled
and onRejected
to define the cases of the asynchronous action. These methods take a callback function that takes the state and the data as parameters and returns the new state.
asyncActions: (builder) => ({
fetchMovies: builder
.use(fetchMovies)
.onPending((state) => {
state.loading = true;
return state;
})
.onFulfilled((state, data) => {
state.movies = data;
state.loading = false;
return state;
})
.onRejected((state, error) => {
state.error = error;
state.loading = false;
return state;
}),
});
It's up to you to choose the method that suits you best. 😊
Step 5: Use the asynchronous action in your application
Now that we have created our asynchronous action, we can use it in our application. To do this, we will use the useAsyncActions
hook.
Let's consider we have a component called Movies
inside the file Movies.tsx
and we want to fetch the movies list when the component is mounted.
import React from "react";
import { useAsyncActions, useSignal } from "@dilane3/gx";
const Movies = () => {
const { fetchMovies } = useAsyncActions("movies");
const { movies, loading, error } = useSignal("movies");
React.useEffect(() => {
(async () => {
const response = await fetchMovies();
console.log(response);
})();
}, []);
return (
<div>
<h1>List of movies</h1>
{loading && <p>Loading...</p>}
{error && <p>{error.message}</p>}
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
</div>
);
};
As you can see, we have used the useAsyncActions
hook to retrieve the fetchMovies
asynchronous action and we have used the useSignal
hook to retrieve the movies
, loading
and error
properties of the signal.
The useAsyncActions
hook takes the name of the signal as a parameter and returns an object that contains all asynchronous actions of the signal like useActions
hook does for simple actions
.
Inside the useEffect
hook, we have called the fetchMovies
asynchronous action and we have displayed the list of movies, the loading state and the error if it occurs.
The fetchMovies
asynchronous action returns a response
with the following structure:
{
status: "PENDING" | "FULFILLED" | "REJECTED", // The status of the asynchronous action
state: any, // The state of the signal
data: any | null, // The data returned by the asynchronous action
error: Error | null // The error if it occurs
}
As you can see, the response
contains the status
of the asynchronous action, the state
of the signal, the data
returned by the asynchronous action and the error
if it occurs.
Why is it useful to have the state of the signal in the response ?
It's useful because sometimes we need to perform some actions with the updated state of the signal after the asynchronous action is executed. For example, we want to display a message to the user when the asynchronous action is executed successfully.
import React from "react";
import { useAsyncActions, useSignal } from "@dilane3/gx";
const Movies = () => {
const { fetchMovies } = useAsyncActions("movies");
const { movies, loading, error } = useSignal("movies");
React.useEffect(() => {
(async () => {
const response = await fetchMovies();
if (response.status === "FULFILLED") {
alert("Movies fetched successfully");
}
})();
}, []);
return (
<div>
<h1>List of movies</h1>
{loading && <p>Loading...</p>}
{error && <p>{error.message}</p>}
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
</div>
);
};
Rather than using statuses like "PENDING"
, "FULFILLED"
and "REJECTED"
you can just import the AsyncActionStatuses
enum and use it to check the status of the asynchronous action.
import React from "react";
import { useAsyncActions, useSignal, AsyncActionStatuses } from "@dilane3/gx";
const Movies = () => {
const { fetchMovies } = useAsyncActions("movies");
const { movies, loading, error } = useSignal("movies");
React.useEffect(() => {
(async () => {
const response = await fetchMovies();
if (response.status === AsyncActionStatuses.FULFILLED) {
alert("Movies fetched successfully");
}
})();
}, []);
return (
<div>
<h1>List of movies</h1>
{loading && <p>Loading...</p>}
{error && <p>{error.message}</p>}
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
</div>
);
};
It's up to you to choose the method that suits you best. 😊
Step 6: Handle errors
We have seen how to handle errors in the previous step but we will see how to handle errors in a more advanced way.
After having defined the cases of the asynchronous action, we can define the error
case of the asynchronous action by using the onRejected
method.
asyncActions: (builder) => ({
fetchMovies: builder
.use(fetchMovies)
.onRejected((state, error) => {
state.error = error;
state.loading = false;
return state;
}),
}),
As you can see, we have used the onRejected
method to define the error
case of the asynchronous action. This method takes a callback function that takes the state and the error as parameters and returns the new state. We have binded the error
to the error
property of the state and we have set the loading
property to false
to indicate that the asynchronous action is no longer loading.
By doing like this, we can handle errors in a more advanced way in our application.
import React from "react";
import { useAsyncActions, useSignal, AsyncActionStatuses } from "@dilane3/gx";
const Movies = () => {
const { fetchMovies } = useAsyncActions("movies");
const { movies, loading, error } = useSignal("movies");
React.useEffect(() => {
(async () => {
const response = await fetchMovies();
if (response.status === AsyncActionStatuses.REJECTED) {
alert(response.error.message);
}
})();
}, []);
React.useEffect(() => {
if (error) {
alert(error.message);
}
}, [error]);
return (
<div>
<h1>List of movies</h1>
{loading && <p>Loading...</p>}
{error && <p>{error.message}</p>}
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
</div>
);
};
We have seen two ways to handle errors in our application.
- The first way is to handle errors by checking the status of the asynchronous action response.
- The second way is to handle errors inside the
useEffect
hook by checking theerror
property of the signal that we have defined in the caseonRejected
of the asynchronous action.
The error
property of the signal is null
when the asynchronous action is not rejected.
Now you know how to handle errors in your application. 😊
It's over for this section. I hope you enjoyed it and that you learned a lot of things. 😊
The next section will be about Hooks.