├── .env.example
├── .eslintrc.json
├── .gitignore
├── .vscode
└── settings.json
├── README.MD
├── app.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.cjs
├── prisma
├── db.sqlite
└── schema.prisma
├── public
└── favicon.ico
├── src
├── app.css
├── app.tsx
├── entry-client.tsx
├── entry-server.tsx
├── env
│ ├── client.ts
│ ├── schema.ts
│ └── server.ts
├── routes
│ ├── index.tsx
│ └── results.tsx
└── server
│ └── db.ts
├── tailwind.config.cjs
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=file:./db.sqlite
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "project": "./tsconfig.json"
5 | },
6 | "plugins": ["@typescript-eslint"],
7 | "extends": [
8 | "plugin:solid/typescript",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/consistent-type-imports": "warn"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .solid
3 | .output
4 | .vercel
5 | .netlify
6 | netlify
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | *.launch
16 | .settings/
17 |
18 | # Temp
19 | gitignore
20 |
21 | # System Files
22 | .DS_Store
23 | Thumbs.db
24 |
25 | .env
26 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "*.css": "tailwindcss"
4 | },
5 | "editor.quickSuggestions": {
6 | "strings": true
7 | },
8 | }
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # Data Loading
2 |
3 | SolidStart has built in methods to fetch and manage your data while also keeping your UI updated. In this guide we are going to talk and explain about how data should be loaded in SolidStart, how to trigger Suspense, create server actions & server data loaders. We are also going to learn about optimistic updates and all the existing methods.
4 |
5 | ## Table Of Content
6 |
7 | First lets understand the SolidStart Data API. Please read each section carefully starting with Route Data.
8 |
9 | - [Route Data](#Route-Data)
10 | - [Server Route Data](#Server-Route-Data)
11 | - [createAsync & Suspense](#createAsync)
12 | - [Server Async Data](#Server-Async-Data)
13 | - [cache](#cache)
14 | - [key](#key)
15 | - [keyFor](#keyFor)
16 | - [preload](#preload)
17 | - [Preload Server Data](#preloading-server-data)
18 | - [Actions](#Actions)
19 | - [SSR Note](#note-form--ssr)
20 | - [Using A Form](#Using-A-Form)
21 | - [.with](#with)
22 | - [Using useAction](#Using-useAction)
23 | - [Server Actions](#Server-Actions)
24 | - [Revalidation](#Revalidating-Data--Preloaded-Data)
25 | - [Using A Response](#returning-a-response-from-the-action)
26 | - [Using The revalidate Function](#Using-The-revalidate-Function)
27 | - [Prevent Default Revalidation](#Disable-Revalidation)
28 | - [Optimistic Updates](#Optimistic-Updates)
29 | - [useSubmission](#useSubmission)
30 | - [filter](#Using-A-Filter)
31 | - [Video Of Usage](#Video-Of-Usage)
32 | - [useSubmissions](#useSubmissions)
33 | - [filter](#Using-A-Filter1)
34 | - [Video Of Usage](#Video-Of-Usage1)
35 |
36 | After learning the basics, ahead over to the [ToDo App](#todo-app) section to see how we can create a fully functioning app with Optimistic Updates & Server Actions in SolidStart.
37 |
38 | - [ToDo App](#todo-app)
39 | - [Demo](#video)
40 | - [Installation](#install)
41 | - [Prefetching The ToDo List](#prefetching-the-todo-list)
42 | - [Create ToDo](#create-todo-action)
43 | - [Update Todo](#update-todo-status)
44 | - [Optimistic Updates](#optimistic-updates-1)
45 | - [Final Code](#results)
46 |
47 | # Route Data
48 |
49 | When wanting to preload data or run some logic like protected route or setting cookies / headers, we can use the `preload` function to define a route data function
50 | Route data is an object that contains the [`preload`](#preload) function you can export from a route (a page: i.e routes/index.tsx) in order to load / preload data. This is also valid for server functions, so you could get a csrf cookie and set it before the page is sent to the client.
51 |
52 | When using routeData, you must provide a `preload` function and consume it by using `createAsync`, please read [createAsync](#createasync) and [preload](#preload), you can also use [cache](#cache) to cache your data functions.
53 |
54 | ```ts
55 | import { cache } from "@solidjs/router";
56 |
57 | const myFn = cache(async () => {
58 | return [1, 2, 3];
59 | }, "myFn");
60 |
61 | export const route = {
62 | preload: () => {
63 | myFn();
64 | },
65 | };
66 | ```
67 |
68 | ## preload
69 |
70 | The preload function is called once per route, which is the first time the user comes to that route.
71 | Using preload functions, fetching the data parallel to loading the route is possible to allow use of the data as soon as possible.
72 | The preload function can be called when the Route is loaded or eagerly when links are hovered.
73 |
74 | Once the preload function is set, we can use [createAsync](#createasync) to get the data (like in this example).
75 |
76 | In this example, we are going to hit up the `/api/notes` endpoint and return the data we received.
77 |
78 | ```tsx
79 | import { Suspense, type VoidComponent } from "solid-js";
80 | import { cache, createAsync } from "@solidjs/router";
81 | import { isServer } from "solid-js/web";
82 |
83 | const myFn = cache(async () => {
84 | return await fetch(
85 | isServer ? "http://localhost:3000/api/notes" : "/api/notes"
86 | ).then((r) => r.json() as unknown as number[]);
87 | }, "myFn");
88 |
89 | export const route = {
90 | preload: () => {
91 | myFn();
92 | },
93 | };
94 |
95 | const Home: VoidComponent = () => {
96 | const data = createAsync(async () => myFn());
97 | return (
98 |
99 |
100 |
{data()?.join(",")}
101 |
102 |
103 | );
104 | };
105 |
106 | export default Home;
107 | ```
108 |
109 | In the example above, the data returns instantly, but we can even make it more intrensting by adding a timeout so we can actually see the `Suspense` working.
110 |
111 | ```ts
112 | const myFn = cache(async () => {
113 | await new Promise((resolve) =>
114 | setTimeout(() => {
115 | resolve(undefined);
116 | }, 3000)
117 | );
118 | return await fetch(
119 | isServer ? "http://localhost:3000/api/notes" : "/api/notes"
120 | ).then((r) => r.json() as unknown as number[]);
121 | }, "myFn");
122 | ```
123 |
124 | The `Home` page will only be rendered after 3 seconds using this Promise `setTimeout` with the Suspense.
125 |
126 | ### Preloading Server Data
127 |
128 | You can create a server function using the `"use server";` pragma, then call it (as you would call a regular function).
129 |
130 | ```ts
131 | const myFn = cache(async () => {
132 | "use server";
133 |
134 | return await fetch(
135 | isServer ? "http://localhost:3000/api/notes" : "/api/notes"
136 | ).then((r) => r.json() as unknown as number[]);
137 | }, "myFn");
138 |
139 | export const route = {
140 | preload: () => {
141 | myFn();
142 | },
143 | };
144 | ```
145 |
146 | ## Server Route Data
147 |
148 | Using the `"use server";` pragma you can turn this function to a server function, the client use case stays exactly the same, we just add this line.
149 |
150 | ```ts
151 | import { cache } from "@solidjs/router";
152 |
153 | const myFn = cache(async () => {
154 | "use server";
155 |
156 | return [1, 2, 3];
157 | }, "myFn");
158 |
159 | export const route = {
160 | preload: () => {
161 | myFn();
162 | },
163 | };
164 | ```
165 |
166 | ## createAsync
167 |
168 | createAsync is a function that transforms an async function into a signal that could be used to read the data returned from the promise & trigger Suspense/Transitions.
169 | createAsync is the main function you should be using when interacting with the data API.
170 |
171 | ```tsx
172 | import { myFnPromise } from "@/somewhere";
173 |
174 | const Home: VoidComponent = () => {
175 | const data = createAsync(async () => await myFnPromise());
176 | return (
177 |
178 |
179 | {/* Reading data() triggers the Suspense*/}
180 |
{data()}
181 |
182 |
183 | );
184 | };
185 | ```
186 |
187 | You can also use the `createAsync` method with [preloaded](#preload) functions but you can also use a server function or any function you want.
188 |
189 | ### Server Async Data
190 |
191 | You can create a server function using the `"use server";` pragma, then call it (as you would call a regular function) using createAsync to render the data on the client side.
192 |
193 | ```tsx
194 | import { Suspense, type VoidComponent } from "solid-js";
195 | import { cache, createAsync } from "@solidjs/router";
196 |
197 | const myServerFn = cache(async () => {
198 | "use server";
199 |
200 | console.log("on server");
201 | return [1, 2, 3];
202 | }, "myServerFn");
203 |
204 | const Home: VoidComponent = () => {
205 | const data = createAsync(async () => myServerFn());
206 | return (
207 |
208 |
209 |
{data()?.join(",")}
210 |
211 |
212 | );
213 | };
214 |
215 | export default Home;
216 | ```
217 |
218 | ## cache
219 |
220 | cache is a higher-order function designed to create a new function with the same signature as the function passed to it. When this newly created function is called for the first time with a specific set of arguments, the original function is run, and its return value is stored in a cache and returned to the caller of the created function. The next time the created function is called with the same arguments (as long as the cache is still valid), it will return the cached value instead of re-executing the original function.
221 |
222 | This function is being used to cache returned data from a function and try to minimize the unnecessary function calls
223 |
224 | Cached functions also provide utils that are useful when retrieving the keys used in cases involving [invalidation](#revalidating-data--preloaded-data): [.key](#key) and [.keyFor](#keyFor)
225 |
226 | ```ts
227 | import { cache } from "@solidjs/router";
228 |
229 | const cachedFn = cache(fn, key);
230 |
231 | // ie
232 | const myFn = cache(({ count }: { count: number }) => {
233 | return count === 2 ? [4, 5, 6] : [1, 2, 3];
234 | }, "myFn");
235 | ```
236 |
237 | In the following example there are two examples functions. One is cached and one is not
238 |
239 | ```ts
240 | import { cache } from "@solidjs/router";
241 |
242 | const nonCachedFn = async () => {
243 | console.log("called");
244 | const data = await fetch("/api/notes").then((d) => d.json());
245 | return data;
246 | };
247 |
248 | const cachedFn = cache(() => {
249 | console.log("called");
250 | const data = await fetch("/api/notes").then((d) => d.json());
251 | return data;
252 | }, "uniqueKey");
253 | ```
254 |
255 | When we call `cachedFn` the first time it will actually console log `called`, but when we call it the second time, it will not print anything, matter of fact it will not even make the request to `/api/notes`, that is because the data returned from it is cached, meaning Solid is smart enough to return the latest data received from this function instead of re-trigerring it.
256 |
257 | ```ts
258 | cachedFn(); // prints called
259 | cachedFn(); // doesn't print anything but returns the data fetched from the previous call (line above)
260 | ```
261 |
262 | If we call `nonCachedFn` which is not cached, it will print called every call.
263 |
264 | ```ts
265 | nonCachedFn(); // prints called
266 | nonCachedFn(); // prints called
267 | nonCachedFn(); // prints called
268 | ```
269 |
270 | ### Server functions with cache
271 |
272 | You can create a server function using the `"use server";` pragma, then you call `cache` like you would with a regular function.
273 |
274 | ```ts
275 | const cachedFn = cache((name: string) => {
276 | "use server";
277 |
278 | console.log("called on server", name);
279 | const data = await fetch("http://localhost:3000/api/notes").then((d) =>
280 | d.json()
281 | );
282 | return data;
283 | }, "uniqueKey");
284 | ```
285 |
286 | Cached functions provide utils that are useful when retrieving the keys used in cases involving invalidation:
287 |
288 | ### .key
289 |
290 | Getting the key of a cached function, useful for revalidating all the data() calls for this function.
291 |
292 | ```ts
293 | const key = cachedFn.key; // "uniqueKey"
294 | ```
295 |
296 | After getting the key you can invalidate all calls by doing:
297 |
298 | ```ts
299 | import { revalidate } from "@solidjs/router";
300 |
301 | revalidate(cachedFn.key);
302 | ```
303 |
304 | Read more about [Data Revalidation](#revalidating-data--preloaded-data)
305 |
306 | ### .keyFor
307 |
308 | Getting a specific key from a cached function for a specific input, useful for revalidating the data() call for this specific input on this specific function.
309 |
310 | ```ts
311 | const key = cachedFn.keyFor("OrJDev"); // ["uniqueKey", "OrJDev"]
312 | ```
313 |
314 | After getting the key you can invalidate this specific call by doing:
315 |
316 | ```ts
317 | import { revalidate } from "@solidjs/router";
318 |
319 | revalidate(cachedFn.keyFor("OrJDev"));
320 | ```
321 |
322 | Read more about [Data Revalidation](#revalidating-data--preloaded-data)
323 |
324 | # Actions
325 |
326 | An action is a function you may use to mutate data, meaning when you want to send, update, delete or interact with a server / lib. This is considered to be a `mutation`.
327 | Actions only work with the `POST` request method, this means forms require `method="post"`
328 | By default all cached functions will be revalidated (re-fetched), event if the action does or doesn't return response, you can change this behavior, read more [here](#disable-revalidation)
329 |
330 | To create an action you simply call the `action` method from Solid Router.
331 |
332 | ```ts
333 | import { action, redirect } from "@solidjs/router";
334 |
335 | const fn = action(myFunction, myUniqueName);
336 |
337 | const callWithForm = action(async (formData: FormData) => {
338 | const name = formData.get("name")!;
339 | console.log(`Hey ${name}`);
340 | return redirect("/success");
341 | }, "myUniqueName");
342 |
343 | const callWithString = action(async (name: string) => {
344 | console.log(`Hey ${name}`);
345 | return redirect("/success");
346 | }, "myUniqueName");
347 |
348 | const callWithRevalidation = action(async () => {
349 | return redirect("/some-page", { revalidate: "abcd" });
350 | }, "myUniqueName");
351 | ```
352 |
353 | ## Note (Form + SSR)
354 |
355 | This requires stable references because a string can only be serialized as an attribute, and it is crucial for consistency across SSR. where these references must align. The solution is to provide a unique name. Meaning, you always have to provide a second argument when you call `action`
356 |
357 | ```ts
358 | action(fn, key); // cool
359 | action(fn); // not cool
360 | ```
361 |
362 | You can either use a Form to call an action or use the `useAction` hook from Solid Router.
363 |
364 | ## Using A Form
365 |
366 | ```tsx
367 | import { type VoidComponent } from "solid-js";
368 | import { action, redirect } from "@solidjs/router";
369 |
370 | const callWithParams = action(async (formData: FormData) => {
371 | const name = formData.get("name")!;
372 | console.log(`Hey ${name}`);
373 | return redirect("/success");
374 | }, "myMutation");
375 |
376 | const Home: VoidComponent = () => {
377 | return (
378 |
379 |
383 |
384 | );
385 | };
386 |
387 | export default Home;
388 | ```
389 |
390 | ### .with
391 |
392 | Actions have a `with` method, this method can be used when typed data is required. This removes the need to use `FormData` or other additional hidden fields. The with method works similar to bind, which applies the arguments in order.
393 |
394 | ```tsx
395 | import { type VoidComponent } from "solid-js";
396 | import { action, redirect } from "@solidjs/router";
397 |
398 | const callWithParams = action(async (name: string) => {
399 | console.log(`Hey ${name}`);
400 | return redirect("/success");
401 | }, "myMutation");
402 |
403 | const Home: VoidComponent = () => {
404 | return (
405 |
406 |
409 |
410 | );
411 | };
412 |
413 | export default Home;
414 | ```
415 |
416 | As you can see, no `FormData` was involved in this action.
417 |
418 | ## Using useAction
419 |
420 | If you don't want to use a form, you can use the `useAction` hook from Solid Router. This consumes the action and return a function you can call from anywhere, with any params you would like.
421 |
422 | ```tsx
423 | import { type VoidComponent } from "solid-js";
424 | import { action, redirect, useAction } from "@solidjs/router";
425 |
426 | const callWithParams = action(async (name: string) => {
427 | console.log(`Hey ${name}`);
428 | return redirect("/success");
429 | }, "myMutation");
430 |
431 | const Home: VoidComponent = () => {
432 | const myAction = useAction(callWithParams);
433 | return (
434 |
435 |
436 |
437 | );
438 | };
439 |
440 | export default Home;
441 | ```
442 |
443 | ## Server Actions
444 |
445 | You can create a server function using the `"use server";` pragma, then you call `action` like you would with a regular function.
446 |
447 | ```ts
448 | import { action, redirect } from "@solidjs/router";
449 |
450 | const callWithParams = action(async (name: string) => {
451 | "use server";
452 |
453 | // this is printed on the server
454 | console.log(`Hey ${name}`);
455 | return redirect("/success");
456 | }, "myMutation");
457 | ```
458 |
459 | ## Disable Revalidation
460 |
461 | By default all cached functions will be revalidated (re-fetched), event if the action does or doesn't return response. You can change this by returning a string as the `revalidate` key.
462 |
463 | ```ts
464 | import { action, redirect } from "@solidjs/router";
465 |
466 | const callWithParams = action(async (name: string) => {
467 | console.log(`Hey ${name}`);
468 | return redirect("/success", { revalidate: "nothing" });
469 | }, "myMutation");
470 | ```
471 |
472 | ## Revalidating Data / Preloaded Data
473 |
474 | Assuming we have a [cached](#cache) function called `myFn`, we can invalidate its data by either:
475 |
476 | ### Returning A Response From The Action
477 |
478 | By returning a response from the action, we can choose which keys to invalidate (if we want any).
479 |
480 | ```ts
481 | import { action, json, reload, redirect } from "@solidjs/router";
482 |
483 | const mutateMyFn = action(async (formData: FormData) => {
484 | const name = Number(formData.get("name"));
485 | await mutateData(name); // assuming we changed something
486 | return json({ done: name }, { revalidate: ["myFn"] });
487 |
488 | //or
489 | return reload({
490 | revalidate: ["myFn"],
491 | });
492 |
493 | //or
494 | return redirect("/", {
495 | revalidate: ["myFn"],
496 | });
497 | });
498 | ```
499 |
500 | A key could also be:
501 |
502 | ```ts
503 | return redirect("/", {
504 | revalidate: ["getAllTodos", getTodos.key, getTodoByID.keyFor(id)],
505 | });
506 | ```
507 |
508 | ### Using The revalidate Function
509 |
510 | Assuming we don't want to define the revalidation logic within the action itself, we can also invalidate it from anywhere we want, so we can use the `revalidate` function from Solid Router.
511 |
512 | ```ts
513 | import { revalidate } from "@solidjs/router";
514 | revalidate([getTodos.key, getTodoByID.keyFor(id)]);
515 | ```
516 |
517 | ## Optimistic Updates
518 |
519 | Optimistic updates are crucial for the best dx, they are used to display the data as if it has already been received while its still fetching the actual data in the background. So the user thinks that our server is fast, while the action is still running.
520 |
521 | Assuming we have this action:
522 |
523 | ```ts
524 | const callWithParams = action(async (name: string) => {
525 | console.log(`Hey ${name}`);
526 | await new Promise((resolve) =>
527 | setTimeout(() => {
528 | resolve(undefined);
529 | }, 3000)
530 | );
531 | return `Hey ${name}`;
532 | }, "myMutation");
533 | ```
534 |
535 | When using [action](#Actions) and wanting to implement optimistic updates, you must use one of the following:
536 |
537 | ### useSubmission
538 |
539 | This function takes two arguments:
540 |
541 | 1. action - The action we created before
542 | 2. filter - Optionally filter out this hook for certain inputs - `([name]) => name !== "OrJDev";`
543 |
544 | After that, it returns useful data & functions such as `clear`, `input`, `result`, `pending`, `error` and more.
545 |
546 | We can use the input property to try and set the optimistic data.
547 |
548 | ```tsx
549 | import { Show, type VoidComponent } from "solid-js";
550 | import { useSubmission } from "@solidjs/router";
551 |
552 | const Home: VoidComponent = () => {
553 | const submit = useSubmission(callWithParams);
554 | return (
555 |
556 |
561 |
562 |
Error
563 |
564 |
565 | {(name) =>
Optimistic {`Hey ${name()}`}
}
566 |
567 | {(text) =>
Result {text()}
}
568 |
569 | );
570 | };
571 |
572 | export default Home;
573 | ```
574 |
575 | So in this example, using the `input` and the `result` property, the optimistic is going to be rendered instantly, while the result returned from the function is going to be rendered after 3 seconds!
576 |
577 | You can also use the other methods to modify the state of this submission
578 |
579 | ```tsx
580 | const SomeUtils = () => {
581 | return (
582 |
583 |
584 |
585 |
586 | );
587 | };
588 | ```
589 |
590 | #### Using A Filter
591 |
592 | In this example, we are going to filter out the fake JDev out there, yack
593 |
594 | ```tsx
595 | import { Show, type VoidComponent } from "solid-js";
596 | import { useSubmission } from "@solidjs/router";
597 |
598 | const Home: VoidComponent = () => {
599 | const submit = useSubmission(callWithParams, ([name]) => {
600 | return name !== "FakeJD";
601 | });
602 | return (
603 |
604 |
609 |
614 |
615 |
Error
616 |
617 |
618 | {(name) =>
Optimistic {`Hey ${name()}`}
}
619 |
620 | {(text) =>
Result {text()}
}
621 |
622 | );
623 | };
624 |
625 | export default Home;
626 | ```
627 |
628 | When calling this function with `FakeJD` as the name, all the proprties will remain null and will not be rendered (input,error, etc), if we call it with any other name, it will be rendered instantly.
629 |
630 | #### Video Of Usage
631 |
632 | [](https://github.com/user-attachments/assets/45973a6a-f841-4677-b0e0-c556a3ab353c)
633 |
634 | ### useSubmissions
635 |
636 | This function is similar to [usesSbmission](#usesubmission), except instead of returning one single object with the submission properties (input,result,etc), it returns an array of all the submissions for this action. So instead of:
637 |
638 | ```ts
639 | {
640 | input: any;
641 | error: any;
642 | }
643 | ```
644 |
645 | its:
646 |
647 | ```ts
648 | Array<{
649 | input: any;
650 | error: any;
651 | }>;
652 | ```
653 |
654 | This function takes two arguments:
655 |
656 | 1. action - The action we created before
657 | 2. filter - Optionally filter out this hook for certain inputs - `([name]) => name !== "OrJDev";`
658 |
659 | ```tsx
660 | import { For, Show, type VoidComponent } from "solid-js";
661 | import { useSubmissions } from "@solidjs/router";
662 |
663 | const Home: VoidComponent = () => {
664 | const submits = useSubmissions(callWithParams);
665 | return (
666 |
667 |
672 |
673 | {([attempt, data]) => {
674 | return (
675 |
694 | );
695 | }}
696 |
697 |
698 | );
699 | };
700 |
701 | export default Home;
702 | ```
703 |
704 | So in this example, using the `input` and the `result` property, the optimistic is going to be rendered instantly, while the result returned from the function is going to be rendered after 3 seconds!
705 |
706 | #### Using A Filter
707 |
708 | In this example, we are going to filter out the fake JDev out there, yack
709 |
710 | ```tsx
711 | import { For, Show, type VoidComponent } from "solid-js";
712 | import { useSubmissions } from "@solidjs/router";
713 |
714 | const Home: VoidComponent = () => {
715 | const submits = useSubmissions(callWithParams, ([name]) => {
716 | return name !== "FakeJD";
717 | });
718 | return (
719 |
720 |
725 |
730 |
731 |
732 | {([attempt, data]) => {
733 | return (
734 |
753 | );
754 | }}
755 |
756 |
757 | );
758 | };
759 |
760 | export default Home;
761 | ```
762 |
763 | When calling this function with `FakeJD` as the name, all the properties will remain null and will not be rendered (input,error, etc), if we call it with any other name, it will be rendered instantly.
764 |
765 | #### Video Of Usage
766 |
767 | [](https://github.com/user-attachments/assets/de2f78ef-29e3-4bf4-93c1-0b937692534c)
768 |
769 | # ToDo App
770 |
771 | Lets use what we learned and build a fully functioning ToDo App in SolidStart.
772 |
773 | ## Install
774 |
775 | First, lets clone the ToDo app template (which already includes the db configuration):
776 |
777 | ```sh
778 | git clone git@github.com:OrJDev/solidstart-data.git todo-app
779 | ```
780 |
781 | After that intall the dependencies:
782 |
783 | ```sh
784 | pnpm install
785 | ```
786 |
787 | And then create a `.env` file with:
788 |
789 | ```
790 | DATABASE_URL=file:./db.sqlite
791 | ```
792 |
793 | ## Video
794 |
795 | [](https://github.com/user-attachments/assets/da621f3f-222c-426d-93ed-cc7ac5523dd6)
796 |
797 | We can start building the actual app.
798 |
799 | ## Prefetching The ToDo List
800 |
801 | Ahead over to `routes/index.tsx` and add the following preload method (this is going to pre-load the todo list and trigger Suspense)
802 |
803 | First lets create the `getTodos` function, since we use the db directly to fetch the todos, we need to mark this function as server function:
804 |
805 | ```ts
806 | import { db } from "~/server/db";
807 | import { cache } from "@solidjs/router";
808 |
809 | const getTodos = cache(async () => {
810 | "use server";
811 |
812 | return await db.todo.findMany();
813 | }, "todos");
814 | ```
815 |
816 | After creating this function, we can use and export the `preload` method in order to load the todos list before the page is actually sent to the client:
817 |
818 | ```ts
819 | export const route = {
820 | async preload() {
821 | await getTodos();
822 | },
823 | };
824 | ```
825 |
826 | We need to consume this function and trigger Suspense by using `createAsync`.
827 |
828 | ```tsx
829 | import { createAsync } from "@solidjs/router";
830 |
831 | const Home: VoidComponent = () => {
832 | const todos = createAsync(() => getTodos());
833 | return (
834 |
835 |
836 |
837 | {(todo) => (
838 |
839 | {todo.text}
840 |
841 |
842 | )}
843 |
844 |
845 |
846 | );
847 | };
848 | ```
849 |
850 | By reading `todos()` we trigger suspense (as mentioned in the [createAsync](#createasync) guide).
851 |
852 | After that we can implement the `createToDo` action.
853 |
854 | ## Create ToDo Action
855 |
856 | We are going to create a server action that takes in a formData, it validates that the formData has `text`, after that it creates a new todo with the provided text. Once the todo is created, we are to going to invalidate the `getTodos` method we preloaded eariler, meaning we are going to refetch the todo list . We are also going to wait 2 seconds before revalidation so we can see the optimistic updates in action.
857 |
858 | ```ts
859 | import { revalidate, action } from "@solidjs/router";
860 |
861 | const createToDo = action(async (formData: FormData) => {
862 | "use server";
863 |
864 | const text = formData.get("text");
865 | if (!text || typeof text !== "string") {
866 | throw new Error("Missing Text");
867 | }
868 | await db.todo.create({ data: { text } });
869 | await new Promise((res) =>
870 | setTimeout(() => {
871 | res(undefined);
872 | }, 2000)
873 | );
874 | await revalidate(getTodos.key);
875 | }, "createToDo");
876 | ```
877 |
878 | Having the action ready, we can create a form that will trigger this action:
879 |
880 | ```tsx
881 | import { useSubmissions } from "@solidjs/router";
882 | import { createSignal } from "solid-js";
883 |
884 | const CreateToDo = () => {
885 | const [text, setText] = createSignal("");
886 | const createSubmissions = useSubmissions(createToDo);
887 | return (
888 |
909 | );
910 | };
911 | ```
912 |
913 | This form will be used to create new todos.
914 |
915 | ## Update ToDo Status
916 |
917 | First lets create an action that takes in two args, one is the checked value (if the todo is completed or not) and the other is the id of the todo.
918 |
919 | ```ts
920 | const update = action(async (chcked: boolean, id: string) => {
921 | "use server";
922 |
923 | await db.todo.update({
924 | where: { id },
925 | data: {
926 | completed: chcked,
927 | },
928 | });
929 | await new Promise((res) =>
930 | setTimeout(() => {
931 | res(undefined);
932 | }, 2000)
933 | );
934 | await revalidate(getTodos.key);
935 | }, "updateToDo");
936 | ```
937 |
938 | After creating this action, we can use it to update a todo. Since the todo completed value is stored within a `checkbox` we won't use a form but rather the `useAction` function, we will also be using the `useSubmissions` hook for optimistic updates & check if function is pending.
939 |
940 | First lets switch the way we render the current todo:
941 |
942 | ```tsx
943 | import { useAction } from "@solidjs/router";
944 |
945 | const updateToDo = useAction(update);
946 |
947 |
948 | {(todo) => (
949 |