I’ve always liked having starter kits for my apps. But ever since Vite came into the scene, I thought having a React with TypeScript one doesn’t really makes sense. Turns out, I was wrong.
Looking back at the previous iteration of this starter, dated at late 2019/early 2020, I vividly remember that the hardest and most annoying part there was the hot reloading. Configuring it with Webpack and TypeScript was really annoying, and even if I was doing it since 2016 or so, I always struggled. So when I’ve switched to Vite, I rendered my starter useless.
The “why”
Recently, as I both started a channel that requires coding examples, and returned to working with clients, I realized how wrong I was. Installing and configuring tools I am always using takes a tremendous amounts of time. Even if I know the code by heart and rarely even consult documentation for Storybook or Plop, I still have to manually enter most of the things.
My last commission was a React application. I started to install the usual tools, like Storybook, Plop, Tailwind, configure routing, tests… and I’ve realized this takes me a lot of time! Why my customers have to pay for me doing the same (boring, mind you) thing?
That is why, during the past weekend, I’ve decided to put together another starter, and make it public. It’s very opinionated, so it most likely won’t be everyone’s cup of tea, but that’s why it is a public repo. You can fork it and make it your own!
The ”what”
If you’ve seen my video on Design Systems Scaffoling, you’ll recognize most of the things I’ve put in here. But not without few surprises.
Architecture
I’ve decided to go with scalable solutions. Every space in here has its own
directory, which exposes a single index.ts
file.
For UI, there’s Tailwind and Atomic Design, so atoms, molecules, organisms and views. Coding the entire view as a prop-augmented solution might be a chore, but testing it then is very easy and straightforward. But I am first to admit that sometimes building a page as one file in the pages directory is just quicker and more productive, given it is small enough.
For API communication, I’ve put React Query in. In my experience, 90% of
projects will still utilize REST, so it’s a safer bet. But adding Apollo for
GraphQL is a breeze. Oh, and I’ve configured it to use the root API defined in
.env
, so whenever you’re doing a fetch, you just need to put the URI, e.g.
/products
instead of the whole URL.
For routing, React Router. I was thinking of going with TanStack Router, but
I’ve went with the popularity here, and given how the former caught up to
Tanner’s solution, I think it sticks. Plus, upcoming transition of Remix might
be a real game changer. But, as with everything here, replacing it is just a
provider away. There’s also pages
directory, which should be utilized as,
well, pages, and, in ./src/App
, there’s the main routing config.
Unfortunately, React Router doesn’t allow for multiple nested routers, so
sometimes you need to export a feature as a route config, and declare it as
children
of a page:
// ./src/App.tsx
...
{
path: "/with-fetch",
element: <ApiPage />,
children: activities,
},
All your pages can be exported either as static imports, or as lazy ones. If you
want to do the former, simply export it from pages/index.ts
. If you’d like to
have them lazy-loaded (which I recommend if the page is larger than a few
words), use pages/lazy.tsx
. In there you can import what you need using the
React’s lazy
:
// ./src/pages/lazy.tsx
import { lazy, Suspense } from "react";
const AnyPage = lazy(() => import("./AnyPage/AnyPage"));
And then use the lazyFactory
function that’ll wrap it in Suspense
:
// ./src/pages/lazy.tsx
export const Any = lazyFactory(AnyPage);
It accepts another parameter, if you’d like to have a custom loading component:
// ./src/pages/lazy.tsx
import { LoadingBar } from "@ui/molecules";
export const Any = lazyFactory(AnyPage, LoadingBar);
For business features, I’ve put the features
directory, in which you
should put all your business-related things, packed in a neat package. There’s
an example that has fetching and routing, but in general, every self-contained
feature should reside there. It can be as small as a cookie popup, or as big as
a sign up flow.
Testing
Tests are divided into two categories: units/integration and e2e.
For unit and integration testing, you should use Vitest. It’s compatible
with Jest (not 100%, which I’ve learn the hard way though), and I’ve added
@testing-library
’s packages to make sure all React and DOM assertions are
there. All .test.ts
and .test.tsx
files will get automatically picked up by
npm t
;
For end-to-end, there’s Playwright. As much as I like Cypress, this looks a
way better alternative. All tests should go to e2e
directory, but you’re free
to expand this. All of the configuration, including base app url, is kept in
./playwright.config.ts
. One thing to notice here, is that I’ve added a script
(npm run test:e2e:mocks
) that will build the app (with mocked server), launch
the localhost, then run the test suites, and, once it’s all done, gracefully
exit.
Oh, yes, mocks. I’ve picked MSW, which in my opinion, should be a standard
for mocking any kind of communication. It has its own mocks
directory, where
you can define new endpoints that’ll be used for both unit and e2e tests. MSW is
bound to run on every unit and integration test, and also can be picked up when
building production app for testing (mentioned test:e2e:mocks
command), or to
work with a dev mode (npm run dev:mocks
). What it does is that it intercepts
all traffic coming from the application and if it finds a suitable handler,
instead of letting the traffic further, it takes uses said handler.
Generators
You might remember Plop, my generator of choice. In here, it’s far more robust, allowing to generate:
- ui elements;
- hooks;
- utilities;
- pages;
- features.
This way you can forget about copy-pasting all these boring files and just generate whatever you need and start working right off the bat.
Linting
First of all, there’s more robust linting using ESLint. It now has more strict
rules, thanks to tseslint.configs.recommendedTypeChecked
, but also… import
sorting. This is something I’ve always liked. Having imports grouped in vendors,
internals, types, all this, out of the box (well, out of the… saving file, I
guess) is just comfortable.
Development and Building
There’s not much changed from the basic Vite config. App builds into a regular bundle that’ll get split if you’re using the lazy imports. Dev mode also works in a standard way, with hot reloading out of the box.
One thing to notice here is dependency offloading. I am pulling React and
ReactDOM, the largest libraries, from CDN. This is handled by vite-plugin-cdn2
plugin, which configuration lies in ./vite.config.ts
.
—
That’s it. My starter for medium-to-large React apps. I invite you to fork, create pull requests and issues. It will most certainly live, as I often find myself adding things to my tooling.