I've been putting off building my own Typescript packages for a while.
Not for a lack of ideas, but because I know modern Javascript/Typescript development is a mess. Just check the size of your node_modules directory after starting a default React project to see what I mean, over 200MB of dependencies just to get started! Or better yet, try to start a React project without create-react-app
.
It would take me days to configure my own Typescript package with Babel, Prettier, Rollup, ESLint, Jest, etc. just the way I want it. Not to mention probably cost me my sanity.
Then I stumbled onto TSDX.
After reading the README, I was able to publish an npm package complete with tests in a single evening.
This guide is a simplified version of what I learned publishing my first package. By the end of this tutorial you should have a published and tested Typescript package in the NPM registry.
NPM Registry
First you'll need to create an NPM account and configure it to use in your command line. Start with this short guide to configure your account and login via the command line with npm login
if you haven't done so.
What we're Building
Since this tutorial is aimed at beginners, we're going to be building something simple. A reusable React component with Jest tests, types, and Github actions:
Truly awe inspiring, I know.
Live Demo
Final Source Code
Setup
Let's bootstrap our TSDX project from the command line:
npx tsdx create toggle
After the dependencies are installed, let's make sure we can start the project in development/watch mode:
cd toggle
npm start
You should now have a functioning package!
File Structure
> tree -L 2 -I node_modules
.
├── LICENSE
├── README.md
├── dist
│ ├── index.d.ts
│ ├── index.js
│ ├── toggle.cjs.development.js
│ ├── toggle.cjs.development.js.map
│ ├── toggle.cjs.production.min.js
│ ├── toggle.cjs.production.min.js.map
│ ├── toggle.esm.js
│ └── toggle.esm.js.map
├── example
│ ├── index.html
│ ├── index.tsx
│ ├── package.json
│ └── tsconfig.json
├── package-lock.json
├── package.json
├── src
│ └── index.tsx
├── test
│ └── blah.test.tsx
└── tsconfig.json
The default project is pretty minimalist. There are a few directories/files that are important to know though.
Directories
- src: This is where all of the source files that will be built live
- example: An example playground to test your component/package
- dist: What will get built and published to npm. You shouldn't really have to touch this directory and it should be excluded from source control.
- test: Your tests
Files
- src/index.tsx: Your main source file that will be built. This needs to import all your other source files
- package.json: Dependencies/all configuration for your package
- example/package.json: Dependencies for your playground (these will not be published to npm)
- example/index.tsx: File that loads your package for the playground
- test/blah.test.tsx: Example test file
- README.md: Generated README with a lot of useful information for reference.
Toggle Component
To keep with React best practices, we'll make a separate file for our component.
Copy and paste the following code into src/Toggle.tsx
:
// Inside src/Toggle.tsx
import React, { FC } from 'react';
export const Toggle: FC = () => {
return (
<label className="switch">
<input type="checkbox" />
<span className="slider round"></span>
</label>
);
};
Nothing crazy here, just a default HTML checkbox. Let's export our component from our index.tsx
file which is the main file that will be used in the package.
// src/index.tsx
export * from './Toggle';
TSDX projects come with an example folder to help you visualize your component in a browser. This is what we'll use as a sandbox for our component as well. Since we changed the name of the component, we'll have to update the example import:
// example/index.tsx
import 'react-app-polyfill/ie11';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Toggle } from '../src/index'; // 👈 Change our import
const App = () => {
return (
<div>
<Toggle />{/* 👈 Change to use your new component*/}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
Now let's run this sandbox environment to see what we have:
cd example
npm i
npm start
Navigate to http://localhost:1234. You should see a checkbox!
Styling
Let's add some styles to our sweet checkbox now. Open a new file called Toggle.css
inside of the src directory and copy the following styles into it:
/* src/Toggle.css */
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
Let's import those styles into our Toggle.tsx
component. We will need to install the rollup-plugin-postcss
to tell Rollup how to compile CSS as part of our package:
npm i -D rollup-plugin-postcss
Now create a file called tsdx.config.js in the root of your project and paste the following code:
// tsdx.config.js
const postcss = require('rollup-plugin-postcss');
module.exports = {
rollup(config, options) {
config.plugins.push(
postcss({
plugins: [],
})
);
return config;
},
};
Now we can import our styles using ESM imports:
// src/Toggle.tsx
import React, { FC } from 'react';
import './Toggle.css'; // 👈 Import our new styles
export const Toggle: FC = () => {
return (
<label className="switch">
<input type="checkbox" />
<span className="slider round"></span>
</label>
);
};
Save and refresh your browser.
Tada!
Component Props
But what if we want to actually do something with the state of our toggle component? It's not very useful as is.
Let's add component props in order to give us more flexibility:
// src/Toggle.tsx
import React, { FC } from 'react';
require('./Toggle.css');
export type ToggleType = {
isOn: boolean;
handleChange: () => void;
};
export const Toggle: FC<ToggleType> = ({ isOn, handleChange }) => {
return (
<label className="switch">
<input checked={isOn} onChange={handleChange} type="checkbox" />
<span className="slider round"></span>
</label>
);
};
We can now pass props into the component and manage the state of it. Our types will automatically be built and included as part of our project, since we are exporting ToggleType
.
Let's update our playground to contain this state and make sure the toggle still works:
// example/index.tsx
import 'react-app-polyfill/ie11';
import React, { useState } from 'react';
import * as ReactDOM from 'react-dom';
import { Toggle } from '../src/index';
const App = () => {
const [isOn, setIsOn] = useState(false);
return (
<div>
<Toggle isOn={isOn} handleChange={() => setIsOn(!isOn)} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
Now we're handling state outside of the component. This means we can change the toggle state anywhere by simply calling setIsOn(!isOn)
.
Tests
We're ready to publish our package, however let's make sure we have a functioning test first. We want people to contribute to your project and we don't want to test the functionality in our sandbox every time a new PR is opened.
Let's rename the blah.test.tsx
file to toggle.test.tsx
and update our react-dom
render method:
// src/tests/blah.test.tsx -> src/tests/toggle.test.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Toggle } from '../src';
describe('it', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<Toggle isOn={false} handleChange={() => {}} />, div);
ReactDOM.unmountComponentAtNode(div);
});
});
In order for Jest to be able to read CSS files, we'll need to install a package to allow us to mock these files:
npm i -D identity-obj-proxy
And then edit our package.json to reflect this:
// package.json
...
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less|scss|sass)$": "identity-obj-proxy"
}
},
...
See Jest docs for more on why this is necessary. We should have a functioning test now, from your root level directory:
npm test
Huzzah!
The only problem is that this is just testing that the component mounts and doesn't break the app doing so. What we really want to test is that the toggle functionality and isOn
prop works.
We can use react-testing-library to test our component prop functionality:
npm i -D @testing-library/react @testing-library/jest-dom
Let's update our test file to use some of these new testing methods. We'll be using the render
and fireEvent
methods:
// test/toggle.test.tsx
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { Toggle } from '../src';
it('Should render the toggle and be clickable between states', () => {
// mock onChange function
const onChange = jest.fn();
const { getByTestId, rerender } = render(
<Toggle isOn={false} handleChange={onChange} />
);
// checkbox and parent label components
const checkbox = getByTestId('Toggle');
const label = getByTestId('Toggle-label');
// isOn=false should mean it's unchecked
expect(checkbox).toHaveProperty('checked', false);
// Clicking from off -> on
fireEvent.click(label);
expect(onChange).toHaveBeenCalledTimes(1);
// isOn=true should mean it's checked
rerender(<Toggle isOn={true} handleChange={onChange} />);
expect(checkbox).toHaveProperty('checked', true);
// Clicking from on -> off
fireEvent.click(label);
expect(onChange).toHaveBeenCalledTimes(2);
});
If this is a bit confusing or if you're unfamiliar with react-testing-library, that's okay. All we're really doing here is rendering the component and making sure isOn
reflects a checked state and that our handleChange
function is called every time on click.
Double check that it still works:
npm test
Publish
You'll want to make sure you update the version, author, and name of your package. The name should be unique and not taken in the NPM registry. There are three fields you need to change in your package.json:
"author": "Frodo Baggins",
"name: "frodo-toggle",
"version": "1.0.0",
The last step is to publish now!
npm publish
If you get an error, it's likely that you either need to a) login again via npm login
or b) change the package name to be unique. If you want to see if the package name is available, try searching for it in the npm registry.
Congratulations, you are now a Typescript package author. 😎
Anyone can now install your package from the command line by running:
npm i your-toggle-lib # replace this with your package name
Next Steps
There are a few things you could do to make this package better from here. If you're planning on allowing outside contributors you may want to tweak the default Github action that comes with TSDX to run your test suite on new PRs. This will make sure that outside contributors are not merging in broken changes.
Other possible next steps:
- Add props to change the color and add labels to the toggle button.
- Add a
size
prop with "small", "medium", and "large" options. - Add different transitions based on a prop.
- Add styled-components instead of css
The world is your oyster!
Configuration is the most painful part of any project, but libraries like TSDX and create-react-app are amazing at lowering the barrier of entry for newcomers and lazy people (like me). No one likes spending a day fighting with configuration files. Hopefully this guide gives you a bit more confidence that you can write your own packages. I look forward to seeing your projects on Github and npm!
Top comments (10)
Would importing the css files as a
require('./index.css')
mean you'd need a custom webpack config on the other end to pull it through? Not sure if thatrequire
would resolve when you used the package. Unsure though, not tried it out myself.I think not using
require
actually would require a custom rollup plugin from the look of it. When using ESM type imports for CSS I get an error, but maybe I'm missing something?Something like: github.com/thgh/rollup-plugin-css-.... I'm kind of a noob with Rollup though, more familiar with Webpack and CRA. If there's a better way to do this without having to install a plugin I'm all ears.
Styled-components would support this natively, I think. That should be fairly easy to drop in to your code examples above. Or a similar css-in-js solution.
Either that, or you'd need to allow users to import the CSS file directly, similar to how
react-toastify
handles it.What's the error you're getting? A TS error or a Rollup error? (I am also pretty clueless with rollup, also a CRA boi)
When running
npm start
:You were right about that, it didn't work correctly when you pulled it from npm. I added the postcss plugin to the tutorial and now it should work as expected. Thanks for the heads up!
Nice, good stuff.
tsdx is really awesome tool for creating npm packages similar to microbundler.
When I use npm start, I get an error, error: Invalid Version: undefined, what should I do
I faced the same problem. Look at this...
github.com/parcel-bundler/parcel/i...
great !
it's fantastic
special thanks for your help
have a good dev;