This is a pretty run of the mill React app. It's written in TypeScript with Vite for bundling and uses React Router for routing.
# Install dependencies
$ npm install # or yarn or pnpm install
# Start dev server
$ npm run dev # or yarn dev or pnpm dev
Keep it simple until it needs to be complicated! Don't over-engineer things, but don't be afraid to refactor when things get messy.
Few component libraries are used, because they cause a lot of bloat and most of the time don't integrate well with each other (UI/UX wise). For these reasons, most components are custom built. If complex functionality is needed, a lightweight hook is preferred over a component library. For example, Floating UI is used as a floating primitive that is used for all floating functionality, such as dialogs, tooltips, dropdowns, etc.
Most components expose the props of their root element, so that the component can be styled with className
and also helps to reduce the number of elements in the DOM (see styling section).
Styling is done with Tailwind. Right now, there are little reusable abstractions and most of the styling is done inline. This is because the app is still in active development and it's hard to know what will be reused and what won't. As the app grows, more abstractions will be made. However, for the few necessary reusable styles base.ts
contains a few styles that are used throughout the app (though see #40). Most components expose and merge (using tailwind-merge
) className
, which should be used over wrapping in a div
and styling that.
Besides the following exceptions, all icons are from Hero Icons and are just used as inlined SVGs:
State is saved where ever it is most convenient (i.e., no big nasty Redux store 🎉):
react-query
which is more like a cache than a state manager. It is usually used through a hook that wraps a useQuery
call (see src/hooks/useLogbooks.ts
for example). However, cache invalidation is a little bit more complicated (big surpise) and is not directly handled by the hook. Instead, when something invalidates a query, it calls queryClient.invalidateQueries
with the appropriate key.zustand
store. This is useful for domain specific state that is not related to the server, but is too complex to be managed by a component. For example, the draft storage is managed by a zustand store. This is because the draft is used in multiple places and is too complex to be managed by a component. However, it is not related to the server, so it doesn't make sense to use react-query
. It is also not hierarchical, so it doesn't make sense to use a context. Thus, zustand is used.Routing is done with React Router. There are two main pages: Home
and Admin
. However, within these pages there are different routes that change the SideSheet
. For example, the route /:entryId
shows the entry with id entryId
in the side sheet along side the the main Home
entry list as well as the navbar.
There are two types of errors: critical and noncritical. Critical errors cause the page to crash and shows an error page. Noncritical errors are recoverable and thus just display a toast. All unhandled errors are critical and cause the page to crash.