repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

starfx / docs / posts
Eric Bower · 18 Aug 24

loaders.md

  1---
  2title: Loaders
  3slug: loaders
  4description: What are loaders?
  5---
  6
  7Loaders are general purpose "status trackers." They track the status of a thunk,
  8an endpoint, or a composite of them. One of the big benefits of decoupled
  9loaders is you can create as many as you want, and control them however you
 10want.
 11
 12[Read my blog article about it](https://bower.sh/on-decoupled-loaders)
 13
 14# Usage
 15
 16For endpoints, loaders are installed automatically and track fetch requests.
 17Loader success is determined by `Response.ok` or if `fetch` throws an error.
 18
 19You can also use loaders manually:
 20
 21```ts
 22import { put } from "starfx";
 23// imaginary schema
 24import { schema } from "./schema";
 25
 26function* fn() {
 27  yield* put(schema.loaders.start({ id: "my-id" }));
 28  yield* put(schema.loaders.success({ id: "my-id" }));
 29  yield* put(schema.loaders.error({ id: "my-id", message: "boom!" }));
 30}
 31```
 32
 33For thunks you can use `mdw.loader()` which will track the status of a thunk.
 34
 35```ts
 36import { createThunks, mdw } from "starfx";
 37// imaginary schema
 38import { initialState, schema } from "./schema";
 39
 40const thunks = createThunks();
 41thunks.use(mdw.loader(schema));
 42thunks.use(thunks.routes());
 43
 44const go = thunks.create("go", function* (ctx, next) {
 45  throw new Error("boom!");
 46});
 47
 48const store = createStore({ initialState });
 49store.dispatch(go());
 50schema.loaders.selectById(store.getState(), { id: `${go}` });
 51// status = "error"; message = "boom!"
 52```
 53
 54# Shape
 55
 56```ts
 57export type IdProp = string | number;
 58export type LoadingStatus = "loading" | "success" | "error" | "idle";
 59export interface LoaderItemState<
 60  M extends Record<string, unknown> = Record<IdProp, unknown>,
 61> {
 62  id: string;
 63  status: LoadingStatus;
 64  message: string;
 65  lastRun: number;
 66  lastSuccess: number;
 67  meta: M;
 68}
 69
 70export interface LoaderState<
 71  M extends AnyState = AnyState,
 72> extends LoaderItemState<M> {
 73  isIdle: boolean;
 74  isLoading: boolean;
 75  isError: boolean;
 76  isSuccess: boolean;
 77  isInitialLoading: boolean;
 78}
 79```
 80
 81# `isLoading` vs `isInitialLoading`
 82
 83Why does this distinction exist? Well, when building a web app with `starfx`,
 84it's very common to have called the same endpoint multiple times. If that loader
 85has already successfully been called previously, `isInitialLoading` will **not**
 86flip states.
 87
 88The primary use case is: why show a loader if we can already show the user data?
 89
 90Conversely, `isLoading` will always be true when a loader is in "loading" state.
 91
 92This information is derived from `lastRun` and `lastSuccess`. Those are unix
 93timestamps of the last "loading" loader and the last time it was in "success"
 94state, respectively.
 95
 96# The `meta` property
 97
 98You can put whatever you want in there. This is a useful field when you want to
 99pass structured data from a thunk into the view on success or failure. Maybe
100this is the new `id` for the entity you just created and the view needs to know
101it. The `meta` prop is where you would put contextual information beyond the
102`message` string.
103
104Here's an example for how you can update the `meta` property inside an endpoint:
105
106```tsx
107const fetchUsers = api.get("/users", function* (ctx, next) {
108  yield* next();
109  if (!ctx.json.ok) return;
110  // this will merge with the default success loader state
111  // so you don't have to set the `status` here as it is done automatically
112  // with the api middleware
113  ctx.loader = { meta: { total: ctx.json.value.length } };
114});
115
116function App() {
117  const loader = useQuery(fetchUsers());
118  if (loader.isInitialLoading) return <div>loading ...</div>;
119  if (loader.isError) return <div>error: {loader.message}</div>;
120  return <div>Total number of users: {loader.meta.total}</div>;
121}
122```