From 46b828083d86bd9932cdb804a789c00b61e38d7d Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe Date: Wed, 29 Jan 2025 13:28:46 -0600 Subject: [PATCH] chore: update README.md to include code style guide Signed-off-by: Ryan Hopper-Lowe --- ui/admin/README.md | 254 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 227 insertions(+), 27 deletions(-) diff --git a/ui/admin/README.md b/ui/admin/README.md index 23b7fb565..61d6d1497 100644 --- a/ui/admin/README.md +++ b/ui/admin/README.md @@ -1,51 +1,251 @@ -# templates/spa +# Code Style Guide -This template leverages [Remix SPA Mode](https://remix.run/docs/en/main/guides/spa-mode) to build your app as a Single-Page Application using [Client Data](https://remix.run/docs/en/main/guides/client-data) for all of your data loads and mutations. +## Api/Data State (`SWR`) -## Setup +Api State is managed 100% with `axios` and `SWR`. -```shellscript -npx create-remix@latest --template remix-run/remix/templates/spa +When creating any api call, first create a route in the ApiRoutes object in `~/lib/routers/apiRoutes.ts`. + +```ts +// ~/lib/routers/apiRoutes.ts + +const ApiRoutes = { + ... + namespace: { + route: (id, { queryParam }) => buildUrl(`/path/to/route/${id}`, { queryParam }) + } +} ``` -## Development +### Queries (GET Requests) + +Actual fetching logic is handled via Api Services in `~/lib/service/api`. + +> Note: GET calls should always be coupled with a `key` and a `revalidate` method! + +```ts +// ~/lib/services/api/namespaceService.ts + +async function fetchData(id: string, queryParam: Nullish) { + const url = ApiRoutes.namespace.route(id, { queryParam }).url; + + return await request({ url, method: "GET" }); +} +fetchData.key = (id: Nullish, queryParam: Nullish) => { + if (!id) return null; // return null if no id is provided, this will prevent `SWR` from triggering the request -You can develop your SPA app just like you would a normal Remix app, via: + // notice the above only checks for the existence of `id` and NOT `queryParam`. + // this is because `queryParam` is not a required parameter for the `fetchData` function, but `id` is. -```shellscript -npm run dev + return { + url: ApiRoutes.namespace.route(id, { queryParam }).path, // always use the url path as a unique identifier (this also makes it easier to revalidate as needed + + // add all other dependencies to the key to ensure there are no cache collisions + id, + queryParam, + }; +}; + +// this revalidate method allows us to invalidate the cache for `fetchData` from anywhere in the application +fetchData.revalidate = createRevalidate(ApiRoutes.namespace.route); + +export const ApiService = { fetchData }; ``` -## Production +We then use `useSWR` to cache the data and manage the api state. + +```tsx +// ~/components/namespace/Component.tsx + +const Component = ({ id, queryParam }) => { + const { data, isLoading, error } = useSWR( + ApiService.fetchData.key(id, queryParam), + ({ id, queryParam }) => ApiService.fetchData(id, queryParam) // id will always be defined here because if it's not, the key will return null and the request will not be triggered + ); + + return
...
; +}; + +const OtherRandomComponent = () => { + return ( +
+ +
+ ); +}; +``` + +### Mutations (POST Requests) + +Mutation methods do not require a `key` or `revalidate` method. + +```ts +// ~/lib/services/api/namespaceService.ts -When you are ready to build a production version of your app, `npm run build` will generate your assets and an `index.html` for the SPA. +async function createData(data: CreateData) { + const url = ApiRoutes.namespace.createData().url; -```shellscript -npm run build + return await request({ url, method: "POST", data }); +} + +export const ApiService = { createData }; +``` + +When using them, it's usually best to wrap them in a `useAsync` hook to get access to various helpers. + +```tsx +const MyComponent = () => { + const { data, isLoading, error } = useAsync(ApiService.createData, { + onSuccess: () => { + // ...logic + }, + onError: () => { + // ...logic + }, + }); + + return
...
; +}; ``` -### Preview +## Application State Management (`Zustand`) + +> 90% of the time `SWR` and custom hooks are more than enough to handle all things state management. Zustand should ONLY be used for things that are inherently complex or need to be shared across the entire application. + +Large state management libraries get way out of hand extremely easily. In order to mitigate this there are certain criteria that must be met to justify using a zustand store. + +This criteria must always be met when using Zustand: + +- (Always) Logic/state is **NOT** data/api related. We use `SWR` for all data/api related logic and using anything else will break the integrity of the api cache. Only Client-application logic should be handled with zustand. + +At least 1 of the below criteria should be met: + +- Logic/state is inherently complex and should be encapsulated +- Logic/state is inherently global +- Logic/state requires React render optimization + +### Examples + +#### Global State Management + +When using zustand globally you can usually just create a store normally: + +```ts +// ~/lib/store/global-store.ts -You can preview the build locally with [vite preview](https://vitejs.dev/guide/cli#vite-preview) to serve all routes via the single `index.html` file: +type GlobalStore = {...} -```shellscript -npm run preview +export const useGlobalStore = create()((set, get) => ({ + ... +})) ``` -> [!IMPORTANT] -> -> `vite preview` is not designed for use as a production server +If this store relies on some sort of data state, you can tape them together like so -### Deployment +```tsx +// ~/lib/store/global-store.ts -You can then serve your app from any HTTP server of your choosing. The server should be configured to serve multiple paths from a single root `/index.html` file (commonly called "SPA fallback"). Other steps may be required if the server doesn't directly support this functionality. +type GlobalStore = {...} -For a simple example, you could use [sirv-cli](https://www.npmjs.com/package/sirv-cli): +const useGlobalStore = create()((set, get) => ({ + ... +})) -```shellscript -npx sirv-cli build/client/ --single +// ~/lib/components/... +const GlobalProvider = ({ children, ...props }: { children: React.ReactNode }) => { + // ...SWR and data logic + const { data } = useSWR(...) + + const { init } = useGlobalStore() + + // if we can find a way to produce the same effect here without duct taping data/application + // stores together, I'm all for it.... I hate this + useEffect(() => { + init(data) + }, [data, init]) + + // notice no context is needed here because the store is already inherently global + return children +} + +// ~/lib/routes/root.tsx + +const App = () => { + return ( + ...providers + + + + ) +} +``` + +#### Local State Management + +When using zustand locally you will need to use the `createStore` function. + +```tsx +// ~/lib/store/local-store.ts + +type LocalStore = {...} + +export const initLocalStore = (...params) => createStore((set, get) => ({ + ...params, + ... +})) + +// ~/lib/hooks/namespace/use-init-local-store.ts +const useInitLocalStore = (...params) => { + const [store] = useState(() => initLocalStore(...params)) + + // any logic that needs to be done when the store is initialized + + + return [ + useStore(store), // reactive store, can be used right away + store // store instance, can be used to pass to context providers + ] +} + + +// ~/lib/components/... + +const StoreConsumer = ({ children, ...props }) => { + const [store] = useInitLocalStore(...props) + + // store is avallable locally + + // you can also pass store properties to context providers if needed +} ``` -## Styling +When you need to pass the store to a context provider you can do so like so: -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. +```tsx +// ~/lib/components/... + +const StoreProvider = ({ children, ...props }) => { + const [_, storeInstance] = useInitLocalStore(...props); + + return ( + + {children} + + ); +}; + +const useLocalStore = () => { + const store = useContext(LocalStoreContext); + + if (!store) throw new Error("Store not found"); + + return store; +}; +```