Loaders

What are loaders?


Loaders are general purpose "status trackers." They track the status of a thunk, an endpoint, or a composite of them. One of the big benefits of decoupled loaders is you can create as many as you want, and control them however you want.

Read my blog article about it

Usage #

For endpoints, loaders are installed automatically and track fetch requests. Loader success is determined by Response.ok or if fetch throws an error.

You can also use loaders manually:

1import { put } from "starfx";
2// imaginary schema
3import { schema } from "./schema";
4
5function* fn() {
6  yield* put(schema.loaders.start({ id: "my-id" }));
7  yield* put(schema.loaders.success({ id: "my-id" }));
8  yield* put(schema.loaders.error({ id: "my-id", message: "boom!" }));
9}

For thunks you can use mdw.loader() which will track the status of a thunk.

 1import { createThunks, mdw } from "starfx";
 2// imaginary schema
 3import { initialState, schema } from "./schema";
 4
 5const thunks = createThunks();
 6thunks.use(mdw.loader(schema));
 7thunks.use(thunks.routes());
 8
 9const go = thunks.create("go", function* (ctx, next) {
10  throw new Error("boom!");
11});
12
13const store = createStore({ initialState });
14store.dispatch(go());
15schema.loaders.selectById(store.getState(), { id: `${go}` });
16// status = "error"; message = "boom!"

Shape #

 1export type IdProp = string | number;
 2export type LoadingStatus = "loading" | "success" | "error" | "idle";
 3export interface LoaderItemState<
 4  M extends Record<string, unknown> = Record<IdProp, unknown>,
 5> {
 6  id: string;
 7  status: LoadingStatus;
 8  message: string;
 9  lastRun: number;
10  lastSuccess: number;
11  meta: M;
12}
13
14export interface LoaderState<
15  M extends AnyState = AnyState,
16> extends LoaderItemState<M> {
17  isIdle: boolean;
18  isLoading: boolean;
19  isError: boolean;
20  isSuccess: boolean;
21  isInitialLoading: boolean;
22}

isLoading vs isInitialLoading #

Why does this distinction exist? Well, when building a web app with starfx, it's very common to have called the same endpoint multiple times. If that loader has already successfully been called previously, isInitialLoading will not flip states.

The primary use case is: why show a loader if we can already show the user data?

Conversely, isLoading will always be true when a loader is in "loading" state.

This information is derived from lastRun and lastSuccess. Those are unix timestamps of the last "loading" loader and the last time it was in "success" state, respectively.

The meta property #

You can put whatever you want in there. This is a useful field when you want to pass structured data from a thunk into the view on success or failure. Maybe this is the new id for the entity you just created and the view needs to know it. The meta prop is where you would put contextual information beyond the message string.

Here's an example for how you can update the meta property inside an endpoint:

 1const fetchUsers = api.get("/users", function* (ctx, next) {
 2  yield* next();
 3  if (!ctx.json.ok) return;
 4  // this will merge with the default success loader state
 5  // so you don't have to set the `status` here as it is done automatically
 6  // with the api middleware
 7  ctx.loader = { meta: { total: ctx.json.value.length } };
 8});
 9
10function App() {
11  const loader = useQuery(fetchUsers());
12  if (loader.isInitialLoading) return <div>loading ...</div>;
13  if (loader.isError) return <div>error: {loader.message}</div>;
14  return <div>Total number of users: {loader.meta.total}</div>;
15}
<< PREV
Middleware
NEXT >>
FX