📜 ⬆️ ⬇️

Starting the React and TypeScript component library


Most of my work, I write backends, but the other day there was a task to start a library of components on React. A few years ago, when the React version was as small as my experience in front-end development, I already took an approach to the projectile and it turned out clumsily and clumsily. Taking into account the maturity of the current React ecosystem and my growing experience, I was inspired this time to do everything well and conveniently. As a result, I had a blank for the future library, and in order not to forget anything and collect everything in one place, this cheat sheet article was written, which should also help those who do not know where to start. Let's see what I did.


TL / DR: Code for a ready-to-start library can be viewed on github


The problem can be approached from two sides:


  1. Find a ready-made starter-kit, boilerplate or cli generator
  2. Collect everything yourself

The first method is good for a quick start when you absolutely do not want to deal with configuring and connecting the necessary packages. Also, this option is suitable for beginners who do not know where to start and what should be the difference between the library and the regular application.


At first I went the first way, but then I decided to update the dependencies and fasten a couple more packages, and then all kinds of errors and inconsistencies rained down. As a result, he rolled up his sleeves and did everything himself. But I will mention the library generator.


Create react library


Most developers who deal with React have heard about a convenient React application starter that allows you to minimize project configuration and provides reasonable defaults - Create React App (CRA). In principle, it could be used for the library ( there is an article on the Habré ). However, the project structure and approach to the development of the ui-kit is slightly different from the usual SPA. We need a separate directory with component sources (src), a sandbox for their development and debugging (example), a documenting and demonstration tool ("showcase") and a separate directory with files prepared for export (dist). Also, library components will not be added to the SPA application, but will be exported through an index file. Thinking about it, I went searching and quickly discovered a similar CRA package - Creat React Library (CRL).


CRL, like CRA, is an easy-to-use CLI utility. Using it, you can generate a project. It will contain:



To generate the library project, we can do it ( npx allows us not to install packages globally):


npx create-react-library 

We answer the proposed questions.

CLR questions


And as a result of the utility, we get the generated and ready-to-work project of the component library.


With a certain structure

Clr tree


And then something went wrong ...

Dependencies are a bit outdated today, so I decided to update them all to the latest versions using npm-check :


 npx npm-check -u 

Another sad fact is that the sandbox application in the example directory is generated in js. You will have to manually rewrite it to TypeScript, adding tsconfig.json and some dependencies (for example, typescript itself and basic @types ).


Also, the react-scripts-ts package is declared deprecated and is no longer supported. Instead, you should install react-scripts , because for some time now CRA (whose package is react-scripts ) supports TypeScript from the box (using Babel 7).


As a result, I did not master the pulling of the react-scripts to my idea of ​​the library. As far as I remember, the Jest from this package required the isolatedModules compiler option, which went against my desire to generate and export d.ts from the library (all this is somehow related to the limitations of Babel 7, which is used by Jest and react-scripts to compile TS ) So I made an eject for react-scripts , looked at the result and redid everything with my hands, which I will write about later.


tsdx


Thanks to the user StanEgo , who spoke about an excellent alternative to Create React Library - tsdx . This cli-utility is also similar to CRA and in one command will create the basis for your library with configured Rollup, TS, Prettier, husky, Jest and React. And React comes as an option. Simple enough to do:


 npx tsdx create mytslib 

And as a result, the necessary fresh versions of the packages will be installed and all settings made. Get a CRL-like project. The main difference from CRL is Zero-config. That is, the Rollup config is hidden from us in tsdx (just like CRA does).


Having quickly run through the documentation, I did not find the recommended methods for a finer configuration or something like eject as in CRA. Having looked at the issue of the project, I discovered that so far there is no such possibility . For some projects, this can be critical, in which case you will have to work a little with your hands. If you don’t need it, then tsdx is a great way to get started quickly.


Take control into our own hands


But what if you go the second way and collect everything yourself? So, let's start from the beginning. Run npm init and generate package.json for the library. Add some information about our package there. For example, we will write the minimum versions for node and npm in the engines field. The collected and exported files will be placed in the dist directory. We indicate this in the files field. We are creating a library of react components, so we rely on users to have the necessary packages - we peerDependencies in the peerDependencies field the minimum required versions of react and react-dom .


Now install the react and react-dom packages and the necessary types (since we will be sawing components on TypeScript) as devDependencies (like all the packages in this article):


 npm install --save-dev react react-dom @types/react @types/react-dom 

Install TypeScript:


 npm install --save-dev typescript 

Let's create configuration files for the main code and tests: tsconfig.json and tsconfig.test.json . Our target will be in es5 , we will generate sourceMap , etc. A complete list of possible options and their meanings can be found in the documentation . Do not forget to include source directory in the include , and add the node_modules and dist directories in the exclude . In package.json specify in the typings field where to get the types for our library - dist/index .


Create the src directory for the source components of the library. Add all sorts of little things, like .gitignore , .editorconfig , a file with a license and README.md .


Rollup


We will use Rollup for assembly, as suggested by CRL. The necessary packages and config, I also spied on the CRL. In general, I heard the opinion that Rollup is good for libraries, and Webpack for applications. However, I did not configure Webpack (what CRA does for me), but Rollup is really good, simple and beautiful.


Install:


 npm install --save-dev rollup rollup-plugin-babel rollup-plugin-commonjs rollup-plugin-node-resolve rollup-plugin-peer-deps-external rollup-plugin-postcss rollup-plugin-typescript2 rollup-plugin-url @svgr/rollup 

In package.json add the fields with the distribution of the collected library bundles, as rollup recommends to us - pkg.module :


  "main": "dist/index.js", "module": "dist/index.es.js", "jsnext:main": "dist/index.es.js" 

Create the configuration file rollup.config.js
 import typescript from 'rollup-plugin-typescript2'; import commonjs from 'rollup-plugin-commonjs'; import external from 'rollup-plugin-peer-deps-external'; import postcss from 'rollup-plugin-postcss'; import resolve from 'rollup-plugin-node-resolve'; import url from 'rollup-plugin-url'; import svgr from '@svgr/rollup'; import pkg from './package.json'; export default { input: 'src/index.tsx', output: [ { file: pkg.main, format: 'cjs', exports: 'named', sourcemap: true }, { file: pkg.module, format: 'es', exports: 'named', sourcemap: true } ], plugins: [ external(), postcss({ modules: false, extract: true, minimize: true, sourceMap: true }), url(), svgr(), resolve(), typescript({ rollupCommonJSResolveHack: true, clean: true }), commonjs() ] }; 

The config is a js file, or rather an exported object. In the input field, specify the file in which the exports for our library are registered. output - describes our expectations on the output - in what format module to compile and where to put it.


Next comes the field with a list and configuration of plugins
  • rollup-plugin-peer-deps-external - allows you to exclude peerDependencies from the bundle to reduce its size. This is reasonable, because peerDependencies is expected from the library user.
  • rollup-plugin-postcss - integrates PostCss and Rollup. Here we disable css-modules, include css in the export package from our library, minimize it and enable the creation of sourceMap. If you do not export any css other than that used by the library components, then extract can be avoided - the necessary css in the components will be added to the head tag on the page as necessary in the end. However, in my case, it is necessary to distribute some additional css (grid, colors, etc.), and the client will have to explicitly connect the css-bundle library to itself.
  • rollup-plugin-url - allows you to export various resources, such as pictures
  • svgr - transforms svg into React components
  • rollup-plugin-node-resolve - defines the location of third-party modules in node_modules
  • rollup-plugin-typescript2 - connects the TypeScript compiler and provides the ability to configure it
  • rollup-plugin-commonjs - converts commonjs dependency modules to es modules so that they can be included in bundle

Add a command in the scripts package.json field to build ( "build": "rollup -c" ) and start the assembly in watch-mode during development ( "start": "rollup -c -w && npm run prettier-watch" ) .


The first component and export file


Now let's write the simplest react component to check how our assembly works. Each component in the library will be placed in a separate directory in the parent directory - src/components/ExampleComponent . This directory will contain all the files associated with the component - tsx , css , test.tsx and so on.
Let's create some styles file for the component and tsx file of the component itself.


ExampleComponent.tsx
 /** * @class ExampleComponent */ import * as React from 'react'; import './ExampleComponent.css'; export interface Props { /** * Simple text prop **/ text: string; } /** My First component */ export class ExampleComponent extends React.Component<Props> { render() { const { text } = this.props; return ( <div className="test"> Example Component: {text} <p>Coool!</p> </div> ); } } export default ExampleComponent; 

Also, in src you need to create a file with types that are common to libraries, where a type will be declared for css and svg (peeped at CRL).


typings.d.ts
 /** * Default CSS definition for typescript, * will be overridden with file-specific definitions by rollup */ declare module '*.css' { const content: { [className: string]: string }; export default content; } interface SvgrComponent extends React.FunctionComponent<React.SVGAttributes<SVGElement>> {} declare module '*.svg' { const svgUrl: string; const svgComponent: SvgrComponent; export default svgUrl; export { svgComponent as ReactComponent }; } 

All exported components and css must be specified in the export file. We have it - src/index.tsx . If some css is not used in the project and is not specified as part of those imported into src/index.tsx , then it will be thrown out of the assembly, which is fine.


src / index.tsx
 import { ExampleComponent, Props } from './ExampleComponent'; import './export.css'; export { ExampleComponent, Props }; 

Now you can try to build the library - npm run build . As a result, rollup starts and collects our library into bundles, which we will find in the dist directory.


Next, we add some tools to improve the quality of our development process and its result.


Forgetting code formatting with Prettier


I hate in a code-review to point out formatting that is careless or non-standard for a project, and even more so argue about it. Such flaws should naturally be fixed, but developers should focus on what and how the code does, rather than how it looks. These fixes are the first candidate for automation. There is a wonderful package for this task - prettier . Install it:


 npm install --save-dev prettier 

Add a config for a little refinement of formatting rules.


.prettierrc.json
 { "tabWidth": 3, "singleQuote": true, "jsxBracketSameLine": true, "arrowParens": "always", "printWidth": 100, "semi": true, "bracketSpacing": true } 

You can see the meaning of the available rules in the documentation . WebStrom after creating the configuration file itself will suggest using prettier when starting formatting through the IDE. To prevent formatting from wasting time, add the /node_modules and /dist directory to the exceptions using the .prettierignore file (the format is similar to .gitignore ). Now you can run prettier by applying formatting rules to the source code:


 prettier --write "**/*" 

In order not to run the command explicitly each time with your hands and to be sure that the code of the other project developers will also be prettier formatted, we add the prettier run on the precommit-hook for files marked as staged (via git add ). For this, we need two tools. Firstly, it is hasky , responsible for executing any commands before committing, pushing, etc. And secondly, it is lint-staged , which can run different linters on staged files. We need to execute only one line to deliver these packages and add launch commands to package.json :


 npx mrm lint-staged 

We can not wait for formatting before committing, but make sure that prettier constantly works on modified files in the process of our work. Yes, we need another package - onchange . It allows you to monitor file changes in the project and immediately execute the necessary command for them. Install:


 npm install --save-dev --save-exact onchange 

Then we add to the scripts field commands in package.json :


 "prettier-watch": "onchange 'src/**/*' -- prettier --write {{changed}}" 

On this, all disputes about formatting in the project can be considered closed.


Avoiding Errors with ESLint


ESLint has long become the standard and can be found in almost all js and ts projects. He will help us too. In ESLint configuration, I trust CRA, so just take the necessary packages from CRA and plug it into our library. In addition, add configs for TS and prettier (to avoid conflicts between ESLint and prettier ):


 npm install --save-dev eslint eslint-config-react-app eslint-loader eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-eslint eslint-config-prettier eslint-plugin-prettier 

ESLint using the configuration file.


.eslintrc.json
 { "extends": [ "plugin:@typescript-eslint/recommended", "react-app", "prettier", "prettier/@typescript-eslint" ], "plugins": [ "@typescript-eslint", "react" ], "rules": { "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-member-accessibility": "off" } } 

Add the command lint - eslint src/**/* --ext .ts,.tsx --fix to the scripts field from package.json . Now you can run eslint through npm run lint .


Testing with Jest


To write unit tests for library components, install and configure Jest , a testing library from facebook. However, since we compile TS not through babel 7, but through tsc, then we need to install the ts-jest package too:


 npm install --save-dev jest ts-jest @types/jest 

In order for jest to properly accept imports of css or other files, you need to replace them with mokami. Create the __mocks__ directory and create two files there.
styleMock.ts :


 module.exports = {}; 

fileMock.ts :


 module.exports = 'test-file-stub'; 

Now create the jest config.


jest.config.js
 module.exports = { preset: 'ts-jest', testEnvironment: 'node', moduleNameMapper: { '\\.(css|less|sass|scss)$': '<rootDir>/__mocks__/styleMock.ts', '\\.(gif|ttf|eot|svg)$': '<rootDir>/__mocks__/fileMock.ts' } }; 

We will write the simplest test for our ExampleComponent in its directory.


ExampleComponent.test.tsx
 import { ExampleComponent } from './ExampleComponent'; describe('ExampleComponent', () => { it('is truthy', () => { expect(ExampleComponent).toBeTruthy(); }); }); 

Add the test - npm run lint && jest command to the scripts field of package.json . For reliability, we will also drive the linter. Now you can run our tests and make sure they pass - npm run test . And so that the tests do not fall into dist during assembly, add the exclude field in the Rollup config plugin to the exclude field - ['src/**/*.test.(tsx|ts)'] . Specify running tests in husky pre-commit hook before running lint-staged - "pre-commit": "npm run test && lint-staged" .


Designing, documenting, and admiring components with a Storybook


Each library needs good documentation for its successful and productive use. As for the library of interface components, not only I want to read about them, but also to see how they look, and it’s best to touch and change them. To support such a Wishlist, there are several solutions. I used to use a Styleguidist . This package allows you to write documentation in markdown format, as well as insert examples of the described React components into it. Further, the documentation is collected and from it a site-showcase-catalog is obtained, where you can find the component, read the documentation about it, find out about its parameters, and also poke a wand into it.


However, this time I decided to take a closer look at his competitor - Storybook . Today it seems more powerful with its plugin system. In addition, it is constantly evolving, has a large community, and will soon also begin to generate its documentation pages using markdown files. Another advantage of the Storybook is that it is a sandbox - an environment for isolated component development. This means that we do not need any full-fledged sample applications for component development (as CRL suggests). In the storybook we write stories - ts-files in which we transfer our components with some input props to special functions (it is better to look at the code to make it clearer). As a result, a showcase application is built from these stories .


Run the script that initializes the storybook:


 npx -p @storybook/cli sb init 

Now make friends with TS. To do this, we need a few more packages, and at the same time we will put a couple of useful add-ons:


 npm install --save-dev awesome-typescript-loader @types/storybook__react @storybook/addon-info react-docgen-typescript-loader @storybook/addon-actions @storybook/addon-knobs @types/storybook__addon-info @types/storybook__addon-knobs webpack-blocks 

The script created a directory with the storybook configuration - .storybook and an example directory that we mercilessly delete. And in the configuration directory we change the extension addons and config to ts . We add addons to the addons.ts file:


 import '@storybook/addon-actions/register'; import '@storybook/addon-links/register'; import '@storybook/addon-knobs/register'; 

Now, you need to help the storybook using the webpack config in the .storybook directory.


webpack.config.js
 module.exports = ({ config }) => { config.module.rules.push({ test: /\.(ts|tsx)$/, use: [ { loader: require.resolve('awesome-typescript-loader') }, // Optional { loader: require.resolve('react-docgen-typescript-loader') } ] }); config.resolve.extensions.push('.ts', '.tsx'); return config; }; 

We’ll tweak the config.ts config a bit, adding decorators to connect add-ons to all our stories.


config.ts
 import { configure } from '@storybook/react'; import { addDecorator } from '@storybook/react'; import { withInfo } from '@storybook/addon-info'; import { withKnobs } from '@storybook/addon-knobs'; // automatically import all files ending in *.stories.tsx const req = require.context('../src', true, /\.stories\.tsx$/); function loadStories() { req.keys().forEach(req); } configure(loadStories, module); addDecorator(withInfo); addDecorator(withKnobs); 

We will write our first story in the component directory ExampleComponent


ExampleComponent.stories.tsx
 import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { ExampleComponent } from './ExampleComponent'; import { text } from '@storybook/addon-knobs/react'; const stories = storiesOf('ExampleComponent', module); stories.add('ExampleComponent', () => <ExampleComponent text={text('text', 'Some text')} />, { info: { inline: true }, text: ` ### Notes Simple example component ### Usage ~~~js <ExampleComponent text="Some text" /> ~~~ ` }); 

We used addons:


  • knobs - allows you to change props in the component displayed in the Storybook in real-time mode. To do this, wrap props in special functions in stories
  • info - allows you to add documentation and description of props to the story page

Now note that the storybook initialization script added the storybook command to our package.json . Use it to run the npm run storybook . The Storybook will assemble and start at http://localhost:6006 . Do not forget to add to the exception for the typescript module in the Rollup config - 'src/**/*.stories.tsx' .


We are developing


So, having surrounded yourself with many convenient tools and preparing them for work, you can begin to develop new components. Each component will be placed in its directory in src/components with the name of the component. It will contain all the files associated with it - css, the component itself in the tsx file, tests, stories. We start the storybook, create stories for the component, and write documentation for it there. We create tests and test. Import-export of the finished component is written in index.ts .


In addition, you can log in to npm and publish your library as a new npm package. And you can connect it directly from the git repository from both master and other branches. For example, for my workpiece, you can do:


 npm i -s git+https://github.com/jmorozov/react-library-example.git 

So that in the library consumer application in the node_modules directory there is only the contents of the dist directory in the assembled state, you need to add the "prepare": "npm run build" command to the scripts field "prepare": "npm run build" .


Also, thanks to TS, auto-completion in the IDE will work.


To summarize


In mid-2019, you can pretty quickly start developing your library of components on React and TypeScript, using convenient development tools. This result can be achieved both with the help of an automated utility, and in manual mode. The second way is preferred if you need current packages and more control. The main thing is to know where to dig, and with the help of an example in this article, I hope this has become somewhat easier.


You can also take the resulting workpiece here .


Among other things, I do not pretend to be the ultimate truth and, in general, am engaged in front-end insofar as. You can choose alternative packages and configuration options and also succeed in creating your component library. I would be glad if you share your recipes in the comments. Happy coding!


')

Source: https://habr.com/ru/post/461439/


All Articles