Upgrading a React and TypeScript app from react-scripts-ts to Create React App v2.1

Recently, the React team updated Create React App with official support for TypeScript. Create React App makes it easy to create web apps with React by keeping the details of the build process out of your way. No need to configure Webpack, Babel, and other tools because Create React App offers very high quality defaults. It’s not a boilerplate, though. You don’t even need to see the build dependencies and scripts, unless you want to, and upgrading to new versions is super easy. TypeScript is a popular extension to JavaScript that adds compile-time type checking. The two combined, in my opinion, provides one of the best developer experiences you can get for modern frontend web development.

Previously, to use TypeScript with Create React App, you had to create your project with the custom react-scripts-ts, created by Will Monk. Starting in version 2.1 of Create React App, TypeScript works out of the box. I have several projects that were started with react-scripts-ts, and I recently decided to start upgrading them to Create React App v2.1. I thought I’d share the steps that I followed and my experiences with this process.

Create a new baseline app

To start, I decided to create a completely new app with Create React App. I figured that it would be a good idea to have a baseline app, where I could compare its files — like package.json and tsconfig.json — with the files from my existing projects. This new app would give me clues about what exactly I need to change to make the existing apps work properly with the new version of Create React App.

To create a new app, I ran the following command in my terminal:

npx create-react-app my-new-app --typescript

Update library dependencies

In the existing project that I was upgrading, some of my dependencies hadn’t been updated in a little while, so the next step I decided on was to update the react, react-dom, typescript dependencies, along with the type definitions, like @types/react, @types/react-dom, @types/node, and @types/jest. After updating the versions in package.json, I ran npm install to download them all.

I left react-scripts-ts untouched for now. The idea was to make sure that the existing project was using the exact same versions of the dependencies as the new baseline project, and it should still build properly with react-scripts-ts. It’s best to ensure that I wouldn’t run into any issues with new versions of React or TypeScript that might confuse me as a worked through any issues caused by the differences between react-scripts-ts and the official react-scripts.

I needed to make a couple of minor tweaks to so that the new version of the TypeScript compiler was happy, so it was a good thing that I took this extra step.

Switch from react-scripts-ts to react-scripts

Once all of my library dependencies matched and the project would still build with react-scripts-ts, it was time to switch to the official react-scripts. Inside package.json, I replaced the react-scripts-ts dependency with react-scripts. I copied the version number of react-scripts from my new baseline project. Then, I needed to update the npm scripts inside package.json to use react-scripts:

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
}

I replaced react-scripts-ts with react-scripts in each of the npm scripts above. Then, inside the build script, I saw that my existing project included a --env=jsdom option, but the new baseline project did not. I removed this option so that they matched.

Update configuration files

Next, I compared tsconfig.json in both projects. I could see some major differences, like outputting ES Modules instead of CommonJS, and preserving JSX so that Babel could handle it instead of letting the TypeScript compiler do everything. I decided that I would simply copy the contents of tsconfig.json from the new baseline project to the existing project and then make a couple of small tweaks. I ended up setting the lib compiler option to ["es2015", "dom"] so that I could access some newer JS APIs, and I set the strict compiler option to false because I haven’t updated my project to use strict mode yet. Otherwise, I used Create React App’s defaults, which all seem to be working well.

Similarly, I compared package.json in the two projects. I had already updated the dependencies and npm scripts, so the differences at this point were very minor. I saw that the new baseline project included two new fields that were not in the existing project, eslintConfig and browserslist. These defaults (which configure eslint and Babel) provided by Create React App seemed fine, so I copied them over.

In theory, the migration process should be done at this point. If all goes well, you should be able to run npm start to launch the development server, or you can run npm run build to create a production build. For my project, I ran into a couple of issues that I needed to resolve, but nothing major.

Fix compiler errors

I use the react-loadable library to load parts of my app at runtime using code splitting. My imports in TypeScript originally looked like this:

import * as ReactLoadable from "react-loadable"

However, there seemed to be a problem with the following code:

let AdminHomeScreen = ReactLoadable({
  loader: () => import("./AdminHomeScreen"),
  loading: LoadingScreen
});

The TypeScript compiler gave me the following error:

Cannot invoke an expression whose type lacks a call signature. Type ‘{ default: Loadable; Map<Props, Exports extends { [key: string]: any; }>(options: OptionsWithMap<Props, Exports>): (ComponentClass<Props, any> & LoadableComponent) | (FunctionComponent<Props> & LoadableComponent); preloadAll(): Promise<…>; preloadReady(): Promise<…>; Capture: ComponentType<…>; }’ has no compatible call signatures.

For some reason, I could not call the ReactLoadable module as a function. It turned out that I needed to change the import to look like this instead:

import ReactLoadable from "react-loadable"

In order to call a module like a function, you need to import the module’s default export. I think this may have been related to switching from CommonJS modules to ES Modules in tsconfig.json.

After fixing this issue, I could successfully run npm start to launch the development server, and I could open my app in a web browser and use it! However, I discovered one last issue that I needed to fix.

Fix production build

While npm start worked for development, I got a strange error when I ran npm run build to create a production build of my React app.

module.js:550
throw err;
^

Error: Cannot find module ‘webpack/lib/RequestShortener’
at Function.Module._resolveFilename (module.js:548:15)
at Function.Module._load (module.js:475:25)
at Module.require (module.js:597:17)
at require (internal/module.js:11:18)
at Object. (C:\Users\josht\Development\karaoke-butler\karaoke\client\node_modules\terser-webpack-plugin\dist\index.js:19:25)
at Module._compile (module.js:653:30)
at Object.Module._extensions..js (module.js:664:10)
at Module.load (module.js:566:32)
at tryModuleLoad (module.js:506:12)
at Function.Module._load (module.js:498:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! react-client@1.0.0 build: `react-scripts build`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the react-client@1.0.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\josht\AppData\Roaming\npm-cache\_logs\2018-11-25T01_57_14_654Z-debug.log

Unfortunately, a quick Google search did not find anyone having the same issue with Create React App. However, I saw some folks using other frameworks, like Vue or Angular, were getting similar errors. Eventually, I found someone who discovered that it was some kind of conflict inside the notorious package-lock.json. Ever since npm started using this file, it’s been a terrible source of problems, in my experience.

Anyway, I deleted package-lock.json and the node_modules folder. Then, I ran npm install to install all of my dependencies from scratch. That did the trick because npm run build started working correctly.

I don’t really know why package-lock.json always seems to cause problems like this, but I find that deleting this file and the entire node_modules folder frequently fixes issues that I run into. I don’t know if I’m doing something wrong, or if it’s a bug in npm.

My project is upgraded

Updating dependencies in some of my web projects can be daunting sometimes. For some of them, I usually need to set a full day aside to fix everything. With that in mind, I was expecting to hit a few more snags in the process of upgrading from react-scripts-ts to the official version of Create React App. However, the changes required in my app were relatively minor. The process felt very smooth, and I’m happy that I’ll be able to easily update to new versions of Create React App in the future as the React team continues to innovate.

About Josh Tynjala

Josh Tynjala is a front end developer, open source contributor, karaoke enthusiast, and he likes bowler hats. You might be familiar with his project, Feathers UI, an open source user interface library for Starling Framework. You should follow Josh on Twitter.

Leave a Reply

Your email address will not be published. Required fields are marked *