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```