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:
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.
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
And as a result of the utility, we get the generated and ready-to-work project of the component library.
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.
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.
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
.
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"
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.
peerDependencies
from the bundle
to reduce its size. This is reasonable, because peerDependencies
is expected from the library user.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.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"
) .
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.
/** * @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).
/** * 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.
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.
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.
{ "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.
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.
{ "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
.
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.
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.
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"
.
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.
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.
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
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:
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'
.
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.
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