Note:
Phasor
is the newFetchable
. It is a more general concept from physics and does not collide with other standard keywords in web development.
Imagine you have an async function, lets call this the runner -
export type UserSearchErrors = AuthError | QueueFullError | NetworkError;
async function searchUsers(searchTerm: string, skip = 0) {
const results = await users()
.search(searchTerm)
.skip(skip)
.fetch()
.then((res) => res.json());
// ... more logic to throw custom errors or return different data
// example:
// const authError: AuthError = {...}
// if(condition) throw authError;
return results.value;
}
You somehow want to use this in your @mwt/async-reducer-state
based state. But it is currently a pain and cumbersome to do that. Here is where the proposed utilities come in.
You can use the new helper function asyncToPhasorState
to easily create a partial reducer and a partial state for this searchUsers
function.
This is how you would do it -
// π 1. create your partial getters using the helper
const { getInitialState, getPartialReducer } = asyncToPhasorState(
'UserSearch',
searchUsers
);
// π 2. strongly typed initial and partial state for your phasor
export const userSearchInit = getInitialState<UserSearchErrors>();
export const userSearchPartial = getPartialReducer<
GlobalState, // π you will create this further below
GlobalActions, // π you will create this further below
UserSearchErrors
>();
// π 3. export types for your partials
export type UserSearchActions = ActionsForPartial<typeof getPartialReducer>;
export type UserSearchState = typeof initialState;
Info:
ActionsForPartial
andStateForPartial
are available to be imported from@mwt/async-reducer-state
.
Now this partial state and reducer types can be used to construct the global state and actions types -
// π 4. use partials to construct global state and actions types
type GlobalState = UserSearchState; // & FooState & BarState & ...
type GlobalActions = UserSearchActions; // | FooActions | BarActions | TelemetryActions ...
We are now ready to add our partials to the global state when using createState
like below -
// π 5. create global state
export const [useGlobalState, dispatch] = createState<
GlobalState,
GlobalActions
>({
initialState: {
...userSearchInit,
// ...init1, ...init2, ...
},
reducer: pipe({
partials: [
userSearchPartial,
// ...partial1, ...partial2, ...
],
}),
});
Info:
pipe
is the new utility that can be used to combine partial reducers into a single reducer. It also supports specifyingsideEffects
and anerrorHandler
.
- You can use
sideEffects
to log telemetry for your app.
Now, we can use the dispatch function to fetch or refetch user search results -
// Now, dispatch those actions!.
dispatch({
type: 'Invoke_UserSearch',
args: ['searchParam'],
});
Info:
Invoke_UserSearch
will run thesearchUsers
function with the arguments['searchParam']
and update the phasor with
phase: Phase.run
if it was previously atPhase.ready
phase: Phase.rerun
if it was previously atPhase.done
orPhase.fail
This is useful to detect when error based retries are happening as opposed to initial fetching of data, or fetching next batch of data of for the same set of information.
You can now use the phasor in your components as you need. Here is an example -
function MyComponent() {
const UserSearch = useGlobalState((state) => state.UserSearch);
useEffect(() => {
if (isPhasor.ready(UserSearch) || isPhasor.settled(UserSearch)) {
dispatch({
type: 'Invoke_UserSearch',
args: ['searchParam'],
});
}
}, []);
if (isPhasor.inRun(UserSearch)) {
return <div>Loading...</div>;
}
if (isPhasor.done(UserSearch)) {
return <div>{UserSearch.value}</div>;
}
if (isPhasor.failed(UserSearch)) {
return <div>{UserSearch.error}</div>;
}
}
Info:
isPhasor
is a new utility that can be used to check the status of a phasor.
You can always use useGlobalState
hook to get the state and use it within the fetcher function like below -
async function searchUsers(searchTerm: string) {
const { UserSearch } = useState.getState();
const skip = isPhasor.done(UserSearch)
? state.UserSearchResult.data.length
: 0;
// note: assume auth is configured somewhere else
// by calling ODataEndpoint.configure(...);
const results = await users()
.search(searchTerm)
.skip(skip)
.fetch()
.then((res) => res.json());
return results.value;
}
You can use jest.spyOn
to mock the fetcher function -
import * as searchUsersImport from './searchUsers';
jest.spyOn(searchUsersImport, 'searchUsers').mockResolvedValue([]);
enum
s are a compile time construct for TypeScript. Although they are available at run time, TypeScript cannot make strong inferences in compile time if we try to mimic the enum's behavior with an object or any other equivalent. So, we have to use a string literal union type instead to make the type
property dynamic and easily controlled by consumers of this library.
Only the first dispatch will have any effect. Because, after that, the phasor would have been in a run
state - and when it is in this state, the partial reducer returns undefined
when Invoke_
action is dispatched. This is to prevent multiple invocations of the fetcher function while in progress. However, if you want to force a re-run, you can dispatch Invoke_
action after the phasor has settled (after done
or fail
state is reached).