Quick start
Let's write some basic logics using Awai, every front-end developer faces every day. Our task is to implement users list app with following functionaly:
- users list
- user details view
- user refetching every 3 seconds
- tracking
We will use some random public mock API. Let's assume we have fetchUsers
and fetchUser
functions already implemented.
You can see the complete working example at playground.
In order to use full potential of Awai you should feel comfortable using Promises.
Installing dependencies
npm install awai awai-react
Creating state
Awai provides two basic nodes for keeping a state - State and AsyncState. We will need both of them. In State we will store currently viewed user id, whereas in AsyncState we will store all users list. Let's not worry about user details view yet.
const activeUserIdState = state(null);
const usersState = asyncState(fetchUsers);
When our app loads, we do not have any user selected, hence we store null
in activeUserIdState
initially.
As an initial value for usersState
we used an async initializer function, which will be used immediately to load data from the API.
For getting state value use get
method, like usersState.get()
. Similarly, you need to use set
method to set the value. For an AsyncState it is possible to set a Promise as a value, which will set state into a pending
status. AsyncState can have one of three statuses: pending
, fulfilled
or rejected
.
If you use get
method on state being initialized, the return value is undefined
.
If you want to be sure about asyncState value, you should use getPromise
method.
If you would like to initialize users list state later, you can assign an empty array as an initial value, and set a users promise whenever data is needed.
const usersState = asyncState([]);
const loadUsers = action(() => {
usersState.set(fetchUsers());
});
Family state
Now, let's create a state, where we will keep users' details. For that purpose Awai provides familyState
node. That node is basically a record of { [id]: State }
or { [id]: AsyncState }
, depending on an initializer function return type. In our case it will be family of asyncStates, since all the data will be loaded from API asynchronously and our initializer returns a promise.
const usersDetailsFamilyState = familyState((id) => fetchUser(id));
At this moment nothing happens with our family state, unless we use getNode(id)
method. This method will check if state for the requested ID exists in our family. If yes, it will return the state node, otherwise it will create a node using fetchUser(id)
as an initial value.
Action
For this example we only need setActiveUserId
action. You may just set state directly using activeUserIdState.set
, but recommended approach is to wrap it with action
in order to get access to action events, which will be needed for triggering scenarios.
const setActiveUserId = action(id => activeUserIdState.set(id));
// const setActiveUserId = action(activeUserIdState.set);
Effect
Effect is used for reacting to states changes and cleanup any effects. We will use it to refetch active user data every 3 seconds, according to our requirements.
effect([activeUserIdState], (activeUserId) => {
if (activeUserId === null) {
return;
}
const userDetailsStateNode = usersDetailsFamilyState.getNode(activeUserId);
const intervalId = setInterval(() => {
const userDetailsPromise = fetchUser(activeUserId);
userDetailsStateNode.set(userDetailsPromise);
}, 3000);
return () => {
clearInterval(intervalId);
};
});
In this effect, if there is no selected user, we do not want to revalidate any data, so we just return. Next we get active user details state node from family using its ID, and revalidate the user with 3s interval, setting it as a value promise.
Notice that we set a promise, not a resolved value. This helps with race conditions since Awai will only take care of last set promise.
Selector
Selector is used for combining multiple states into some value. Resulting selector is async if any of dependencies is async. Selector is sync otherwise.
const activeUserDetailsState = selector(
[activeUserIdState, usersDetailsFamilyState],
async (activeUserId, _userDetailsFamily) => {
if (activeUserId === null) {
return null;
}
return usersDetailsFamilyState.getNode(activeUserId).getPromise();
},
);
Now we have an async selector, which reacts to any changes in dependencies states and call our callback, passing it state values as arguments, in order to combine resulting value. In our case we pick userDetails state node from familyState by id and return it's promise, so that asyncSelector can handle it.
As you can see, we have ignored _userDetailsFamily
, it is done for a safety reasons, since state with some id may still not be available in family, whereas getNode(id)
assures its existence.
Notice, that even though all the dependencies are sync, you can still use an async combining callback, which will result in async selector:
selector(
[syncState1, syncState2],
async (value1, value2) => {
delay(1000);
return value1 + value 2;
}
);
Tracking
For tracking it's best to use Scenarios, which are a handy tool to react to any AwaiEvents, promises or their combination. And helps to extract tracking/additional logics from business logics.
scenario(setActiveUserId.events.invoked, (id) => {
console.log(`Open details for user with id ${id}`);
});
scenario(activeUserDetailsState.events.requested, (id) => {
console.log(`Requesting newest details for active user`);
});
scenario(usersState.events.rejected, () => {
console.error('Error while loading user');
});
scenario(activeUserDetailsState.events.rejected, () => {
console.error('Error while loading user details');
});
Please note that this is a very primitive usage of Scenarios, as they are one of most powerful parts of Awai. See Scenario docs in order to see different use cases.
React integration
This library provides hooks for connecting Awai's state nodes with React components.
useSetState - returns a
state.set
method (can be used directly).useStateValue - returns curent state value. It works with suspense and ensures that async node is loaded.
useState - Returns a tuple
[useStateValue(state), useSetState(state)]
, just to be aligned with React'suseState
interface.useAsyncStateValue - this hook only works with
ReadableAsyncState
(eg. AsyncState, or AsyncSelector) and returns a result ofgetAsync
method. UnlikeuseStateValue
this hook does not suspend. That means that component is rendered even though state is not yet initialized, which results invalue
to be possiblyundefined
.