diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..539e4c7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# mockup/maquette-not part of the core project +moq/** linguist-vendored diff --git a/.gitignore b/.gitignore index 817ef4b..712b6ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ esb-run.mjs -/.data +.data dist .solid +.vinxi .output .vercel .netlify diff --git a/README.md b/README.md index 1b2bbc3..317f202 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,1706 @@ # SolidStart Notes (basic) -Updated for SolidStart v0.4.11 (new beta, [first beta version](https://github.com/peerreynders/solid-start-notes-basic/tree/2fe3462b30ab9008576339648f13d9457da3ff5f)). -The app is a port of the December 2020 [React Server Components Demo](https://github.com/reactjs/server-components-demo) ([LICENSE](https://github.com/reactjs/server-components-demo/blob/main/LICENSE); [no pg fork](https://github.com/pomber/server-components-demo/)) but here it's just a basic client side routing implementation. +> … in a Zone … where we can get like a lot of the benefits of RSCs without RSCs … + +— [Musing on HTML Partials](https://youtu.be/N-QwFFqI8aQ?t=12170) + +> … you don't have to have server components to have the same benefits that server components give … + +— [What Comes After GraphQL?](https://youtu.be/gfKrdN1RzoI?t=14516) + +Updated for SolidStart v0.5.10 (new beta, [first beta version](https://github.com/peerreynders/solid-start-notes-basic/tree/2fe3462b30ab9008576339648f13d9457da3ff5f)). +The app is a port of the December 2020 [React Server Components Demo](https://github.com/reactjs/server-components-demo) ([LICENSE](https://github.com/reactjs/server-components-demo/blob/main/LICENSE); [no pg fork](https://github.com/pomber/server-components-demo/), [Data Fetching with React Server Components](https://youtu.be/TQQPAU21ZUw)) but here it's just a basic client side routing implementation. It doesn't use a database but stores the notes via the [Unstorage Node.js Filesystem (Lite) driver](https://unstorage.unjs.io/drivers/fs#nodejs-filesystem-lite) . This app is not intended to be deployed but simply serves as an experimental platform. The longer term goal is to eventually leverage island routing to maximum effect once it's more stable and documented ([nksaraf](https://github.com/nksaraf) already demonstrated that [capability](https://github.com/solidjs/solid-start/tree/3f086d7660a6e29dea649e80ea5a7d2fc1ff5910/archived_examples/notes) ([live demo](https://notes-server-components.vinxi.workers.dev/)) with a non-standard branch of SolidStart). + +--- + +## Route Design +The original's demo routing is managed by inside a single context ([`route.js`](https://github.com/reactjs/server-components-demo/blob/95fcac10102d20722af60506af3b785b557c5fd7/src/framework/router.js)) managing the following data: +- A content cache +- `location` consisting of: + - `selectedId` + - `isEditing` + - `searchText` + +This “triple” is used as a key to cache server content for that `location`. +The `location` is exposed in the URL as the [encoded URI component](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) value of the `location` [search parameter](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams); e.g.: + +```TypeScript +const location = { selectedId: 2, isEditing: false, searchText: '' }; +const searchParams = new URLSearchParams([ + ['location', JSON.stringify(location)], +]); +const base = 'http://localhost:4000/react'; +console.log(`${base}?${searchParams.toString()}`); +// "http://localhost:4000/react?location=%7B%22selectedId%22%3A2%2C%22isEditing%22%3Afalse%2C%22searchText%22%3A%22%22%7D" +console.log(`${base}?location=${encodeURIComponent(JSON.stringify(location))}`); +``` + +- `refresh(response)` purges/reinitializes the content cache within a [transition](https://react.dev/reference/react/startTransition); while the next rendering has been initiated with fresh data from the server, the existing UI remains intact, fully capable of interrupting the current render with another state update. + +- `navigate(location)` updates the `location` state within a transition. + +- The `useMutation` hook sends the `payload` associated with `location` to the `endpoint` then using the response to `refresh` the content cache. The hook's state reflects the status of the fetch (`isSaving`) and stores the last error. + +It needs to be explicitly stated: the RSC demo *does **not** support [SSR](https://www.patterns.dev/react/server-side-rendering/)*. +RSCs render [`ReactNodes`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/f1f24cebc663e157637c343ca61766d5a9e00384/types/react/index.d.ts#L424C1-L436C1) that can either be directly inserted into the client's vDOM or deliver prop values to client side components. +The vDOM diffing process then manipulates the DOM accordingly. At no point is there any HTML that needs to be [hydrated](https://dev.to/this-is-learning/why-efficient-hydration-in-javascript-frameworks-is-so-challenging-1ca3); if needed, SSR and hydration is handled by the meta-framework (and its client side code). + +The demo only employs [CSR](https://www.patterns.dev/react/client-side-rendering/); the value proposition of RSCs is that server components have access to server resources while their code is not part of the client bundle, instead the RSC client runtime has to be included in addition to React to support the deserialization of data streamed from RSCs. + +Any keys necessary for SSR need to appear in the path. So the **new** path-based routing becomes: + +- `/?search=`**`:searchText`** i.e. `{selectedId: undefined, isEditing: false, searchText?}` +- `/new?search=`**`:searchText`** i.e. `{selectedId: undefined, isEditing: true, searchText?}` +- `/notes/`**`:selectedId`**`?search=`**`:searchText`** i.e. `{selectedId, isEditing: false, searchText?}` +- `/notes/`**`:selectedId`**`/edit?search=`**`:searchText`** i.e. `{selectedId, isEditing: true, searchText?}` + +Note that `:selectedId` and `:searchText` can vary independently. In a typical usage scenario `:selectedId` will come from a `:searchText` search result but once `:selectedId` is referenced in the path, `:searchText` is free to change and return a result that does **not** include `:selectedId`. +Consequently the server functions are separate: +- `getBriefs`: fetches the note briefs that match `:searchText`. +- `getNote`: fetches the details of the `:selectedId` note. + +```TypeScript +// file: src/api.ts +import { action, cache, redirect, revalidate } from '@solidjs/router'; +// … +import { + deleteNote as deleteNt, + getBriefs as getBf, + getNote as getNt, + upsertNote as upsertNt, +} from './server/api'; +// … +import type { NoteBrief, Note } from './types'; +// … +const getBriefs = cache<(search: string | undefined) => Promise>( + async (search: string | undefined) => getBf(search), + NAME_GET_BRIEFS +); + +const getNote = cache<(noteId: string) => Promise>( + async (noteId: string) => getNt(noteId), + NAME_GET_NOTE +); +// … +export { getBriefs, getNote, editAction }; +``` + +Both of these functions are wrapped in [`@solidjs/router`](https://docs.solidjs.com/reference/solid-router/components/router)'s [`cache()`](https://docs.solidjs.com/reference/solid-router/data-apis/cache). The page is fully server rendered on initial load but all subsequent updates are purely client rendered. +But the router's `cache()` tracks the currently loaded `:noteId` and `:search` keys; so rather than running **both** `getBriefs` and `getNote` server fetches, the router will only use the one whose key has actually changed (or both if both have changed). + +So only the portion of the page that needs to change is updated on the client for `navigate()` even when the path changes. +The `search` parameter affects the content of the `