RETROSPECTIVE

November 5th, 2020

Building a Reusable React Component Library

React

JavaScript

JSS

TypeScript

Front-end applications, specifically those created with React, are built with small reusable JavaScript functions called components. Each component renders and handles the business logic for a small chunk of the application. Components have props, state, and elements that render on the screen.

For developers working on multiple React applications or working for an organization that uses React, there are benefits to using reusable component libraries. Component libraries are shared amongst applications and contain generic components such as buttons, accordions, and form elements. Besides the obvious benefit of code reuse, component libraries help enforce organizational style guides and allow developers to easily iterate on components used in all their applications. Component library setup is easy and can save a lot of front-end development time in the long run.

jarombek-react-components |-- .storybook |-- components | |-- src | |-- stories | +-- test +-- dist

The directory structure shown above matches my reusable component library. There are three top level directories. .storybook holds configuration for storybook, a visualization tool for components. I use storybook to both view and test the functionality of my reusable components. components holds React component source code, tests, and storybook stories. Storybook stories display and serve as documentation for a component. dist is the bundled and minimized code of the reusable component library. This code is used by applications that utilize the component library.

Inside the components directory of my reusable component library is an index.js file which exports all the components in the src directory. index.js is the entry point for the library.

Inside the src directory, each component has its own directory. Components are split into multiple files: <ComponentName>.js, styles.js, and index.js.

For example, I have a reusable component called AJSwitch. All my reusable components are prefixed with AJ so that they are easy to recognize in application code. A switch is a toggle with an on and off state, just like a typical light switch in a home.

AJSwitch has three source code files: AJSwitch.js, styles.js, and index.js. AJSwitch.js contains the AJSwitch component function. styles.js defines the styles for the component written in JSS. index.js exports the component function to the rest of the library.

Below is an abbreviated code snippet of the component function declared in AJSwitch.js. Most of the omitted code determines which styles and classes are applied to the switch (depending on whether it's toggled on/off).

import React, {useState} from 'react'; import PropTypes from 'prop-types'; import {createUseStyles} from 'react-jss'; import styles from './styles'; const useStyles = createUseStyles(styles); /** * Component representing an off and on switch. The user can plug in custom logic which will occur * when the off/on state changes. * @param onChange Function that is called when the switch is toggled. The function takes a single * boolean argument, whose value is {@code true} if the switch is on and {@code false} if the * switch is off. * @param initialState The initial on/off state of the switch. * @param disabled Whether clicking on the switch changes its state. * @param className Custom class attribute(s) attached to the component. * @return {*} React elements representing a toggleable switch. */ const AJSwitch = ({onChange, initialState=false, disabled=false, className}) => { const classes = useStyles(); const [state, setState] = useState(initialState); const onClick = () => { if (!disabled) { const newState = !state; setState(newState); if (typeof onChange === 'function') { onChange(newState); } } }; return ( <div className={mainClass} onClick={onClick}> <div className={headClass}> </div> <div className={tailClass}> </div> </div> ); }; AJSwitch.propTypes = { onChange: PropTypes.func.isRequired, initialState: PropTypes.bool, disabled: PropTypes.bool, className: PropTypes.string }; export default AJSwitch;

The JSS styles for the component are imported from styles.js and created with createUseStyles() and a useStyles() React hook1. Below is the stylesheet for AJSwitch.

export default { ajSwitch: { display: 'flex', height: '20px', width: '36px', cursor: 'pointer' }, ajSwitchDisabled: { cursor: 'default' }, ajSwitchActive: { flexDirection: 'row-reverse' }, ajSwitchInactive: { flexDirection: 'row' }, ajSwitchHead: { position: 'absolute', height: '20px', width: '20px', borderRadius: '50%', boxShadow: '0 2px 2px 0 rgba(0, 0, 0, 0.24)' }, ajSwitchHeadActive: { backgroundColor: '#4b6cc9' }, ajSwitchHeadInactive: { backgroundColor: '#f5f5f5' }, ajSwitchTail: { height: '100%', width: '100%', borderRadius: '10px' }, ajSwitchTailActive: { backgroundColor: 'rgba(75, 108, 201, 0.5)' }, ajSwitchTailInactive: { backgroundColor: '#ccc' } };

JSS is a library for writing CSS styles in JavaScript code. I've found JSS to be a very flexible framework, with its greatest strength being the ability to adjust styles dynamically. With JSS, any JavaScript value or object can be passed to the stylesheet object. This process is further simplified with the React-JSS library; state, props, or other variables can be passed to the JSS object, allowing the styles to change as the React component updates2. I plan to dedicate a future article to JSS where I will discuss its benefits and drawbacks compared to more traditional approaches like CSS or a CSS preprocessor.

The final file, index.js, simply exports the component using ES6 modules.

import AJSwitch from './AJSwitch'; export default AJSwitch;

Storybook is a visualization tool which allows components to be tested, viewed, and documented in isolation from the rest of an application. It can be customized and re-styled to match your company brand or personal taste.

I use Storybook for a couple of reasons. First, Storybook is helpful during a components development process for debugging and testing out styles. Second, it is useful for trying all the different props that a component can take in. Third, it serves as helpful documentation and allows users to search for components they want to use in their applications.

Storybook contains multiple stories, with each story visualizing different configurations of a single component. For example, I have a story for the AJSwitch component defined in a stories directory in storiesAJSwitch.js. It shows two different AJSwitch component configurations.

import React, {useState} from 'react'; import {storiesOf} from '@storybook/react'; import {AJSwitch} from '../src'; storiesOf('AJSwitch', module) .add('default', () => <AJSwitch onChange={state => console.info(`AJSwitch state: ${state}`)} disabled={false} /> ) .add('disabled', () => <AJSwitch onChange={state => console.info(`Disabled AJSwitch state: ${state}`)} initialState={true} disabled={true} /> );

Storybook is easily started locally on its own server with the start-storybook -p 6006 command (the port can be changed to your liking).

Although I decided to write my React component library in JavaScript, many of the applications that use it are written in TypeScript. To avoid any type errors when using the component library in a TypeScript application, I created an index.d.ts file. *.d.ts files are used by TypeScript to provide type information for JavaScript files2. They also help IDEs to properly autocomplete component imports and props when used in applications.

For example, the AJSwitch component has the following type information defined in index.d.ts.

import {FunctionComponent} from "react"; // AJSwitch component export interface AJSwitchProps { onChange: Function; initialState?: boolean; disabled?: boolean; className?: string; } export const AJSwitch: FunctionComponent<AJSwitchProps>;

The AJSwitchProps interface provides type information for the props passed to the AJSwitch component.

Finally, component typing information must be made available to applications that utilize the library. This is accomplished by adding a typings (or types) field to package.json. typings points to the type declaration file, which in my case is index.d.ts3.

{ ... "typings": "./index.d.ts" }

When I release a new version of the component library, I add a tag to its GitHub repository. This tag is then referenced in an applications package.json file. For example, my SaintsXCTF web application’s package.json file has a dependency listed for my component library with a specific tag. Here is how that dependency definition looks:

{ ... "dependencies": { "jarombek-react-components": "git://github.com/ajarombek/jarombek-react-components.git#v0.3.7", ... } }

Reusable component libraries reduce a developers workload when they are programming multiple UI applications. My reusable React components have allowed me to quickly create both full production frontend applications and small prototypes. You can find my component library code on GitHub.

[1] "JSS integration with React", https://cssinjs.org/react-jss/?v=v10.4.0

[2] "What is the main usage of index.d.ts in Typescript?", https://stackoverflow.com/a/51517448

[3] "Publishing: Including declarations in your npm package", https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#including-declarations-in-your-npm-package