Anatomy of a scrollspy component with React and TypeScript (2/2)

Improving the reusability of the component, and publishing the package on npm.

Tom Allen
Frontend Weekly
8 min readDec 17, 2018

--

In the first part of this tutorial, we created a basic scrollspy component. We will now make some slight improvements before preparing and publishing the component to npm. In the process, we will set up a basic test with the Jest framework and Enzyme library.

Improvements

In our first version of the component, we allowed users to pass in class names which would let them style the different states of the scrollspy elements as they see fit. In this section, we are going to take this customizability a step further by allowing users to pass in JSX elements to the scrollspy component. This will allow them to replace the <ul> and <li> elements of the scrollspy with whatever they think is more appropriate.

Adding new props

The first thing we need to do is update our interface and our default props to allow for the new options.

export interface ScrollspyProps {
...
containerElement?: JSX.Element;
itemElement?: JSX.Element;

}
public static defaultProps: Partial<ScrollspyProps> = {
containerElement: <ul />,
itemElement: <li />

};

Now our component will take any kind of JSX element, and will default to using the familiar <ul> and <li> elements we have been using to this point. The type JSX.Element can be null, meaning that even though we set default props for the elements, we still have to check for them when rendering or TypeScript will complain.

Update the render method

I like to break the component render method into smaller chunks where possible because I find it helps when making changes in the future but it’s not strictly necessary. In this case we are going pull out the item list into its own , method, renderItems(), and call that from the component render method itself.

private renderItems() {
const { itemElement, activeItemClassName, itemClassName } =
this.props;

return this.state.items.map((item, k) => {
return itemElement ? React.cloneElement(itemElement,
{key: k,
className: cn(
itemClassName,
item.inView ? activeItemClassName : null
),
onClick: () => this.scrollTo(item.element),
children: item.element.innerText
}): null;
});
}

This function,renderItems(), is quite similar to the bulk of our original render method for our scrollspy. The big change is that we are making use of React.cloneElement() to pass our css classes and on click function down to an arbitrary JSX element that has been passed in to the scrollspy as the itemElement prop. This is done by passing an object containing the props as the second parameter of the React.cloneElement() function.

We are going to repeat this same trick in the main render method to render the containing element.

public render() {
const { itemContainerClassName, containerElement } = this.props;
return containerElement?
React.cloneElement(containerElement,
{
className: cn(itemContainerClassName),
children: this.renderItems()
})
: null;
}

Here we are cloning the JSX element of the container and passing in the function we just wrote as the children of the cloned element. Now users of the scrollspy can pass in their own components.

Publishing our component

There are a few articles out there already that go over the details of publishing a React component using TypeScript. I found this one very helpful in understanding the overall process. There are a lot of things to be aware of, and the configuration options are extremely varied depending on what build tools and frameworks you use, and even which versions of tools you are using. This can make the whole process a source of frustration, especially if you don’t have a template setup already.

After going through a bunch of different tutorials, I came across a tool called create-react-library which did most of what I wanted out of the box and also included typescript support. It is based on create-react-app. Interestingly, it uses Rollup instead of Webpack, which I found to be a blessing because I didn’t have to touch that part of the configuration at all, whereas all of the Webpack examples I found needed a lot of modification to work properly.

Generate the project

First, let’s install create-react-library (CRL) and generate a new project called ‘react-scrollspy-ez’

yarn global add create-react-library
create-react-library react-scrollspy-ez

Just like create-react-app, CRL will ask you to answer a few questions before generating the project. Be sure to choose the ‘typescript’ option.

Bring in our existing component

Because we are using the existing code from the first part of the tutorial, there are a few things we need to do in order to get our code into the project generated by create-react-library.

The first thing is to create a src/components directory, and bringing in our Scrollspy component.

cd react-scrollspy-ez
mkdir src/components
touch src/components/Scrollspy.tsx

Basically, we can paste everything we have written in the first tutorial into Scrollspy.tsx, but we are going to make a couple of small changes to the new project’s configuration and the component.

First, we are going to add two lines to the compilerOptions of the tsconfig.json generated by CRL.

compilerOptions: {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
...
}

These allow us to use import statements in a slightly different way. We will need to change the top of our our Scrollspy.tsx to read:

import React from React

The whole component should now look like this:

Because we have a dependency on the classnames package, let’s make sure we remember to add that, and the types to go along with it.

yarn add classnames
yarn add --dev @types/classnames

Next, we can remove a few of the files that we won’t be using for this particular component from the /src directory: delete test.tsx, index.css, and typings.d.ts.

As a final step for getting our content into the generated project, we should update the index.tsx in the /src directory to make sure it is exporting our component correctly. The file should read:

import Scrollspy from "./components/Scrollspy";
export default Scrollspy;

If we had more complicated components with several named exports, we could add them in here by following this guide.

Set up tests

Testing is both a good thing to do and an annoying thing to setup. CRL does most of the setup for us, provided we play along with some conventions. We want to be able to use the Enzyme library for our tests, so lets install those and the required typings.

yarn add --dev enzyme
yarn add --dev enzyme-adapter-react-16
yarn add --dev @types/enzyme-adapter-react-16
yarn add --dev @types/enzyme

Now, we can create our test file:

mkdir src/__tests__
touch src/__tests__/scrollspy.test.tsx

We should also update the tsconfig.json to exclude the test directory. We can achieve this by adding it to the ‘exclude’ line like this:

"exclude": ["node_modules", "build", "dist", "example", "rollup.config.js","**/__tests__/"] 

Write the tests

We won’t write anything complicated for now, but we should at least check that the component will render. We can check this by using enzymes shallow method once we have wrapped it in a bit of boilerplate. Here is the contents of scrollspy.test.tsx

import Scrollspy from "../components/Scrollspy";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import React from "react";
Enzyme.configure({ adapter: new Adapter() });describe("Scrollspy", () => {
it("is truthy", () => {
expect(Scrollspy).toBeTruthy();
});
it("shallow renders", () => {
shallow(<Scrollspy />);
expect(component.find("ul")).toBeTruthy;
});
});

Enzyme.configure just wires Enzyme up to the React adapter we just installed. The shallow render will let us check that the component initialises without any problems — here we just check that it does indeed have the default <ul> we expect, but we could make this test more elaborate quite easily. Let’s test the component with:

yarn test

… hopefully everything went well. If you want to write more thorough component tests, by following this guide for instance, you should be in a good position to do so.

Create an example of the component in action

One of the nice things about CRL is that it includes an example project and the ability to publish it on github pages very conveniently. This lets us show potential users what the component might look like, and gives us another place where we can check for errors.

The example project is itself written with create-react-library. It doesn’t use TypeScript, but I don’t think that is a big deal. In fact, it allows us to make sure that our component works nicely in the most common React environment.

We need to update two files, /example/src/App.js and /example/src/index.css to import and use our component. I will not post the whole App.js because it is mostly just html. The important lines to update are:

import Scrollspy from 'react-scrollspy-ez'...<Scrollspy 
ids={["one", "two", "three"]}
itemContainerClassName="scrollSpyContainer"
activeItemClassName="active"
itemClassName="spyItemClass"
/>

In the index.css, we just want to add some styling for the classes we have just passed into our component in App.js. When we have done that, let’s see how it looks. First run:

yarn predeploy

This will compile our component, and stick the compiled version in the /dist directory so our example can use it. Next, start the example application by navigating to the example directory and starting the server (just as you would with create-react-app):

cd example
yarn start

Your browser should show you something not too dissimilar to this. If it looks alright, we can easily deploy the example to github by installing gh-pages, and running the deploy script:

yarn add --dev gh-pages
yarn deploy

Doing this assumes that you have a github repo you are using for the component already, otherwise you will need to set one up.

Final checks and publishing the component

Before we release our component to the wild, lets run a few final checks. First, lets verify that the package is going to contain what we expect. We can do this by using yarn pack which will create a zip file. Move this somewhere out of our working directory (so that we don’t end up committing it by mistake), extract it, and check the contents for anything weird.

Next, let’s check that we can install the package.

npm install . -g

This will make sure that we can install the package globally. I had to switch to npm in place of yarn for this and the next command because yarn seems to have some problems both with finding packages in the local directory, and with logging into npm correctly.

The next thing to do is to login to your npm account with:

npm login

For some reason, yarn login was not checking for a password, and thus I wasn’t able to submit the package. However, using npm login created the necessary token and I was able to switch back to yarn for the final step (hopefully yarn will fix these issues soon):

yarn publish

We should get a message saying that the operation was a success, followed shortly thereafter with an email from the good people at npmjs confirming that our package is indeed published and available for use.

Conclusion

Over the course of this tutorial, we have taken a component from conceptual requirements all the way to publishing a reusable component on npm. I was a bit surprised by how difficult it is to get all the different parts — React, TypeScript, Enzyme, Jest — to play nicely together when it comes to publishing a component. Since it is a publicly shared component, it might also be worth setting up a linter using tslint to enforce code style. I have not done this here because it would involve rewriting the component depending on the particular linting preset used.

Future improvements to the workflow should also include updating the Readme with better documentation, updating the components with JSDoc, and setting up continuous integration on the github repository.

--

--