Today, I am going to show you how to build a React library with TypeScript. Let's get started!
- How to build a React library with TypeScript
- Table of contents
- Prerequisites
- Practices
- 1. Create new project with package.json file
- 2. Configure lerna
- 3. Create new package packages/my-react-package
- 4. Install peerDependencies for packages/my-react-package
- 5. Configure TypeScript for
my-react-package - 6. Configure Rollup to bundle our package
- 7. Declare module definition in the packages/my-react-package/package.json file
- 8. Write code for our package
- 9. Bundle
- Usages
- Conclusion
- References
lerna- A tool for managing JavaScript projects with multiple packages.Yarn workspace- Setup NodeJS workspaceReact Components- Basic knowledge about React Components- Understand NodeJS module system
ECMAScript modules - ESMandCommonJS - cjs.
UPDATES:
- 2022–01–29: Add CSS/SCSS modules supports when building React library with TypeScript and Rollup. Check the updates on section 5 (Configure TypeScript for my-react-package) and section 8 (Write code for our package).
{
"name": "learn-to-build-react-package",
"private": true,
"description": "Learn to to build react package",
"keywords": [],
"author": "PhatNguyen <phatnt.uit@gmail.com> (https://phatnguyenuit.github.io)",
"workspaces": [
"examples/*",
"packages/*"
],
"devDependencies": {
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
"lerna": "^4.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
privateshould be turned totrueworkspacescontains workspace paths. I usepackages/*to provide that my packages should be implemented under thepackagesfolder, andexamples/*for all examples with my built libraries
./learn-to-build-react-package
| |-- package.json
| |-- examples
| | |-- example-app
| | | |-- package.json
| |-- packages
| | |-- my-react-package
| | | |-- package.json{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "independent"
}- Notes:
npmClient: whethernpmoryarnclient called when running lerna commanduseWorkspacesuse workspace flagversion: we should chooseindependentto make every single package in our workspace has an independent version, not the same)
mkdir packages/my-react-package;
cd packages/my-react-package;
npm init -y;npx lerna add react --scope my-react-package --peer;
npx lerna add react-dom --scope my-react-package --peer;Why do we use peerDependencies ?
=> It is because our package scope is just a MODULE that can be installed by any projects and which must have our package peerDependencies installed also.
Now our my-react-package peerDependencies section in the package.json file looks like below
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
}{
"compilerOptions": {
"outDir": "lib/esm",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom", "es2016", "es2017"],
"jsx": "react-jsx",
"declaration": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*.ts*"],
"exclude": ["node_modules", "lib"]
}Some highlights:
outDirstands for output directory after compiled TypeScript to thetargetECMAScript versiones5- Must use
"jsx": "react-jsx"to use JSX compiler - Turn on
declarationto extract type definitions - Folder "node_modules" and "lib" must be excluded while compiling TypeScript to JavaScript.
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}These above types support for importing css modules, scss modules.
-
Install devDependencies
my-react-package:~ yarn add -D rollup typescript rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-postcss postcss node-sass -
Create new file
my-react-package/rollup.config.jsimport typescript from 'rollup-plugin-typescript2'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import postCSS from 'rollup-plugin-postcss'; import pkg from './package.json'; export default { input: 'src/index.ts', output: [ { file: './lib/cjs/index.js', format: 'cjs', }, { file: './lib/esm/index.js', format: 'es', }, ], external: [...Object.keys(pkg.peerDependencies || {})], plugins: [ nodeResolve(), commonjs(), typescript({ typescript: require('typescript'), }), postCSS({ plugins: [require('autoprefixer')], }), ], };
-
Our module exposes two types of module system: CommonJS - cjs and ECMAScript - ESM
-
All packages in the
peerDependenciessection will be treated as external dependencies. It means Rollup does not include them in the bundling process. -
We use some Rollup plugins:
- @rollup/plugin-node-resolve resolves modules located in node_modules
- @rollup/plugin-commonjs converts CommonJS modules to ES6 modules to be included in Rollup
- rollup-plugin-typescript2 supports TypeScript
- rollup-plugin-postcss compiles PostCSS in Rollup, with autoprefixer we can gain auto prefixes for all css styles for all browsers automatically. if you want to use SCSS/SASS you must install
node-sassadditionally
{
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"files": [
"/lib"
],
}- Here we define the main file for our project is
"./lib/cjs/index.js" - Our package also expose esm module at
"./lib/esm/index.js" - Type definitions will be included at
"./lib/esm/index.d.ts" - The last important is
"files", which tells NPM which files or folders will be packaged. For our package, it will be"lib"folder
-
my-react-packages/src/hello/index.tsx
import React from 'react'; // Import css modules import cssClasses from './styles.module.css'; // Import scss modules import scssClasses from './styles.module.scss'; export interface HelloProps { name: string; } const Hello: React.FC<HelloProps> = ({ name }) => ( <div className={scssClasses.helloScss}> <p className={cssClasses.helloCss}>Hello, {name}</p> </div> ); export default Hello;
-
my-react-packages/src/hello/styles.module.css
.helloCss { margin: 0; padding: 0; color: red; font-weight: bold; font-size: 2rem; }
-
my-react-packages/src/hello/styles.module.scss
$with: 600px; $height: 200px; .helloScss { display: flex; align-items: center; justify-content: center; width: $with; height: $height; border: 1px solid red; border-radius: 8px; }
-
my-react-packages/src/index.ts
export { default as Hello } from './hello'; // export the default export from './hello', this is Hello component export * from './hello'; // export all named exports from './hello' like HelloProps
-
In the
my-react-package/package.jsonfile add some useful commands:{ "scripts": { "prepack": "yarn build", "build": "rollup -c", "watch": "rollup -cw" } }prepackRun before packing library into a package file. Eg:packages/my-react-package/my-react-package-1.0.0.tgzbuildBuild source codewatchWatch & build changes
-
In the root workspace
package.jsonfile add some useful commands:{ "scripts": { "build": "lerna run build --scope my-react-package", "watch": "lerna run watch --scope my-react-package", "package": "lerna exec --scope my-react-package -- npm pack" } }buildRun buildmy-react-packagewatchWatch & build changesmy-react-package. This is helpful command in development process to build library if any changes occur.packagePackmy-react-packageinto a package file
-
Create new React App (prefer to TypeScript template) under
./examplesfolders:create-react-app example-app --template typescript
-
Install package
my-react-packageintoexample-app:npx lerna add my-react-package --scope example-app
I use
lerna addcommand here to getmy-react-packageinstalled intoexample-appand have latest source code ofmy-react-packageif any new bundles -
Now just import and see how it works on
examples/example-app/src/App.tsx:import { Hello } from 'my-react-package'; import './App.css'; function App() { return ( <div className="App"> <Hello name="Fast" /> </div> ); } export default App;
In the development phase, you can write code for my-react-package and example-app parallelly by using:
-
Start
example-app:cd examples/example-app; yarn start;
-
Watch and build our package my-react-package if any changes
yarn watch
So now, when you want to develop new components or hooks or anything else, you just write the code and new bundle will be built automatically.
To sum it up, there are 9 steps to build a React Library with TypeScript:
- Create new project with package.json file
- Configure lerna
- Create new package packages/my-react-package
- Install peerDependencies for packages/my-react-package
- Configure TypeScript for
my-react-package - Configure Rollup to bundle our package
- Declare module definition in the package.json file
- Write code for our package
- Bundle
Last but not least, thank you for reading through this section! I hope you find this article helpful and solve your concerns when trying to build a React library.
Here is my full example code on GitHub repositories. Reaching to it if you want to explore more.
If you have any questions or feedback, do not hesitate to leave a comment in the box below.
Thank you and see you next time!