Hands-on Change Data Capture in Go with Postgres and Debezium
Explore a hands-on guide to Change Data Capture in Go with Postgres, Apache Pulsar, and Debezium. Learn to create applications that become reactive...
Ori's journey from CRA to Vite.js: The challenges we faced, the benefits we reaped, and why we felt the need to make the shift.
For a long time the canonical way to start a new React project was via the create-react-app
command line utility provided by the React team. In their updated documentation they now recommend frameworks provided by the community. Since then the web has been awash with blog posts and guides to move away from CRA and adopt a modern alternative.
Under the hood CRA provides a lot of configuration, presets and utilities: A dev environment with hot-module-reloading, testing, Typescript support (if you chose so), consistent formatting, ESlint support, integration of image files and probably more. No doubt about it: life as a React developer is good.
Of course, there are downsides: Experienced developers complain about the lack of customisation options. With CRA you get a ton of configuration for free, but your options to override the defaults are limited. If you need some advanced Webpack features, you just can't do it with CRA. Some 3rd-party NPM packages exist to extend the configuration for specific use cases but that introduces yet another dependency with an uncertain future and one has to ask why CRA has to be so restrictive in the first place.
Since esbuild and other ESM based bundlers popped up on the scene Webpack is also being criticised for its performance. Waiting for the dev server to start was just one of the things we took for granted; after all, we only did it once at the start of a session and then HMR took care of incremental updates. Webpack didn't suddenly become slower, but once there was an alternative that could bring up the dev environment within seconds we were all less inclined to accept the initial startup time.
CRA has also become a bit dated and some of its numerous dependencies specify rather old package versions. Keeping CRA up to date doesn't seem to be a high priority for the maintainers. Vite.js comes with a lot of features integrated so we don't have to rely on so many additional NPM packages. In a previous project our direct and indirect dependencies were reduced by 1400 packages by migrating from CRA to Vite.js.
And then there is the always present FOMO: When all your tech media channels are full of CRA alternatives, it's easy to feel left out. We don't think this is a good enough reason to give up your existing setup but we do encourage a critical review of the status quo in your project. Try out other bundlers in a PoC projects and then evaluate the benefits and downsides for your production project.
For us at Ori the need to migrate arose when we encountered timeout issues during our CI test runs. Our test suite completed when run locally on our laptops but regularly timed out when run on Github. This could have been fixed easily with the Jest timeout
configuration option but CRA does not expose this as an option. So we started looking for alternatives and - based on previous experience - focused on evaluating Vite.js.
CRA - Create React App is a collection of settings and scripts to develop, test and build a Javascript or Typescript application without having to go through all the necessary configuration. It was a great starting point when we set up our project a few years ago but is admittedly getting out of touch.
Our application is bundled with different parameters depending on which environment it is built and deployed to, all of which are managed with environment variables. We need to make sure that these still are applied correctly after the migration.
The test stack includes Jest, React Testing Library, and Mock Service Worker (MSW). We also use Storybook to preview our components and work offline when the API is not yet available (thanks to MSW). These tools need to remain usable and stable.
Replacing the foundations of your build framework is a daunting task. Not many know the inner workings of Webpack, Vite.js, etc. well enough to set up a new build configuration from scratch. The rest of us relies on templates and migration guides.
It may be tempting to define the migration goal as a seamless transition from one bundler to the other. Considering the extent of the existing CRA configuration we expected that an alternative tool would not replicate all the settings we were used to immediately. We knew to aim low and set achievable targets that could be reached with a few days of work. We set our acceptance criteria for the initial migration to the following:
You should notice one important omission from the list above: Testing. Our existing tests use Jest as the test runner. Jest alone does not support Typescript. If we wanted to continue using it, we had to set up Babel or ts-jest on top of the Vite.js configuration. That meant additional work for the initial migration and that our test code would be treated differently from the code running in our development environment.
Considering our options, we thought that we should give Vitest a go instead. Vitest uses the same configuration file as Vite.js. This eliminates the extra configuration files and should ensure that the code that is tested is the same code that is bundled into the JS package. Vitest is a rather young project, but it already supports all the features we need. So we set an additional stretch-goal:
An additional consideration was that our deployment pipeline is based on the main
branch of the frontend code. We have multiple development environments but they all run the same code at various stages of maturity. We could not just create a branch for the migration and dedicate a development environment until we worked out the kinks with the new bundler. Once the changes for Vite.js were committed, we had to see the conversion through until we had the deployment pipeline working again - or roll back and stick with CRA.
To minimise the impact, one of our engineers used his laptop to implemented and verify all migration goals except for the actual deployment part. We then had an opportunity to test and complete the deployment when our stakeholders agreed to a 'consolidation sprint' that was dedicated to engineering tasks only. That gave us enough time to test the deployment and go through a few rounds of debugging if necessary.
In the end, the Vite.js configuration worked the first time around, and we found no issues with the new bundle in our development environments.
We read several blog posts and tutorials about migrating from CRA to Vite and began the migration process by following this one by Cathal Mac Donnacha that matched our project particularly well. Here are a few additional comments:
react-app-env.d.ts
to vite-env.d.ts
Our react-app-env.d.ts
contained global type definitions. So instead of just replacing it with an empty vite-env.d.ts
file we moved our custom types over to the new file and used the opportunity to clean up definitions that were no longer used. We also have a top-level types
folder with TS type definition files and moved some of our types into that location.
REACT_APP_
environment variables to VITE_
At the time of the migration our development environments and the production bundle were set up with environment variables. These needed to be renamed from REACT_APP_
to VITE_
. That's an easy enough update using search and replace.
On reviewing the use cases for these variables we discovered that most of them were quick hacks to inject a runtime value into the frontend code. Since then we have eliminated environment variables from our code and replaced them with virtual
API endpoints. But that will be the subject of another blog post.
Our build pipeline generates the React app Javascript bundle and builds a Docker image based on Nginx to server the HTML and JS files. This image is then deployed into multiple environments. The environment specific configuration is done with environment variables applied to the container. So the only way to confirm a working deployment was to create new image, deploy it and try to access or web app online.
To reduce the risk we first built the container image locally using the same Dockerfile that is used during CI. With the right environment variables we were able to connect the React app served by this container to the API provided by one of our online development environments.
With that confirmation we merged our development branch with the Vite.js based build setup into the main
branch. This triggers our release process and after a few minutes we were able to confirm that the new UI version was deployed and could be used to log in to all of our development environments.
We had successfully completed the essential part of our migration. Now we needed to restore our ability to run tests locally during development and in CI as part of our QA and deployment pipeline.
To use Vitest, we installed the required packages:
We then configure Vitest to use jsdom
as environment. Vitest supports many of the configuration options you may be used to from Jest. So we can finally increase our test timeout
value; the one setting that prompted our migration in the first place.
Jest injects its test functions into the global namespace, so describe
or test
are available without explicitly importing them into a file. You can configure Vitest to do the same (globals
config option) but we prefer a clean global namespace and instead added explicit imports of the used functions to each test file. That was the biggest effort during the migration but it is in no way challenging.
Test mocks created with jest.fn()
can simply be replaced with vi.fn()
. We also had to replace jest.requireActual
with vi.importActual
in places where we spy on library functions (react-router-dom
specifically).
With these updates most of our tests were working again, both locally and in our Github CI pipeline. The later part was especially exciting because we could now just rely on the status of the test step instead of a close inspection of the test logs to understand if the failure was due to a random timeout or an actual error.
This was the primary goal of our migration and we had achieved it without any unforeseen issues along the way.
Cathal proposes the following update to the scripts
section in package.json
:
Did you notice the tsc &&
in the build
step? Vite.js does no type checking during the build process. It just transforms TS files to JS and merges them into the necessary chunks. If you want to have some assurance that your files don't contain any type errors you can run tsc
before the the vite build
command and abort if tsc
encounters an error. This slows down your build but you can be sure that no type inconsistencies sneak into your JS bundle.
To enable 'live' type checking in the preview dev environment, we installed the vite-plugin-checker plugin and added the following to vite.config.ts
:
This runs a type checking process in a dedicated thread in the preview environment and during tests. When you preview code and you introduce an error you now get an overlay in your browser window that displays the error message and source code location.
As a CRA user you may not have set up custom linting rules and relied on the quite sensible defaults. These defaults will be gone now. Even if you had custom rules defined, they would have extended the defaults and you now have to replicate the missing base rules.
We tried to base our rule set on the popular AirBnB rules, but found that they no longer provide a significant benefit over the combination of Typescript and VSCode's format-on-save based on Prettier. These two combined eliminate most - if not all - potential Javascript errors that the AirBnB rules used to address. Beyond that, we found that AirBnB rules were too opinionated for our code base and would have required far too many exceptions or updates to make them worthwhile.
We based our setup on this list of most recommended rules:
On top of that we have a few exceptions defined in the rules
section. You should embrace the opportunity to define your own set of linter rules that are supported by your entire team. There is no "one size fits all".
To update Storybook we removed the addons @storybook/preset-create-react-app
and @storybook/builder-webpack5
builder we were using. Instead we installed @storybook/react-vite
and updated the configuration in .storybook/main.ts
:
While the startup time for the dev server or Storybook is now probably below 1 second, the time it takes until the browser has loaded all files that are required to render a page is significant. This is especially noticeable when we reload a page during a session, instead of relying on HMR to update the content.
Looking at the network tab in the browser, it's easy to understand why: Every single source file is requested, redirected and finally successfully loaded. Our project requires around 1000 unbundled files every single time a page is loaded. After multiple reloads in Chrome, the page becomes unresponsive and we have to close the browser tab and reopen another one to continue our session. There may be ways to improve this via lazy loading of dependencies, but we haven't found the time to investigate this further.
We also noticed that a local preview of Storybook does not always completely load the dependencies for a story in the right order, or a story does not update when dependencies finally become available. This may be related to our heavy use of MSW to provide mock API responses for our stories. Together with the long time for a full page reload our developer experience here is somewhat worse that it was before.
All things considered we think that the upgrade has improved our project setup:
Our migration from CRA to Vite.js was just supposed to make our CI test runs more reliable. Updating our build system to Vite.js gave a us an opportunity to look at our setup from a new perspective. In the process we have gained a better understanding of our tools and their settings. That new knowledge improves our project more than the fixed CI pipeline.
Explore a hands-on guide to Change Data Capture in Go with Postgres, Apache Pulsar, and Debezium. Learn to create applications that become reactive...
Ready to experience the Snowflake-Arctic-instruct model with Hugging Face? In this blog we are going to walk you through environment setup, model...
Explore how to integrate Ori with your existing CI/CD pipelines.