A story of a shared components library 

Andrey Semin

Senior Software Engineer

at MicroMech

https://twitter.com/ifedyukin

https://www.instagram.com/semin_andrey

Hello Andrey!

Has anyone tried to build your own components library?

Why we decided to build this library?

  • Build more than 1 app
  • Attract more customers
  • Provide same look & feel

Starting point

  • App built with MaterialUI 🤮
  • Sometimes components from MUI were wrapped with custom components
  • Sometime they were not
  • No documentation
  • MUI-styled components API
  • Weird or missing tests

Requirements

  • Simple and handy component API
  • Framework-independent API
  • Written in TypeScript
  • Well documented
  • Well tested
  • Include components showcase
  • Built on fresh MUI version

How exciting it is!

  • Extract basic components to separate package
  • Drop lots of dependencies from the app
  • Remove weird tests
  • Make everything "right"

Cotton

Component folder

  • index.ts
  • ComponentName.tsx
  • ComponentName.spec.tsx
  • ComponentName.stories.tsx
  • ComponentName.README.md
  • components/

Component migration

  • Search for all usages of a component
  • List all used props and their values
  • Use prepared list to design component API
  • Migrate component

How to bundle?

Webpack!

Time to test!

  • Write tests from scratch
  • Jest + @testing-library/react ⚡
  • External component API
  • Separate snapshot for each render path

Time for Storybook!

How MaterialUI uses styles


const typographyStyles = (): StyleRules => createStyles({...})

const TypographyComponent: React.FC<TypographyProps> = () => {...}

export const Typography = withStyles(typographyStyles)(TypographyComponent)

css-in-js 👋 

*.md for the rescue!

A component to add an empty space with controllable height based on default theme gutter value

| Name             | Required | type   | Default Value | Description                         |
| ---------------- | -------- | ------ | ------------- | ----------------------------------- |
| gutterMultiplier | -        | number | 1             | Multiplier to default theme gutter  |
| data-test-id     | -        | string | -             | Custom data-test-id attribute value |

### Example:

```js
import { Spacer } from '@motokaptia/cotton';

<>
  <SomeBlock />
  <Spacer />
  <SomeOtherBlock />
</>;
```

*.md for the rescue!

We have to ship parts of MUI itself

  • ThemeProvider component
  • createGenerateClassName function
  • withStyles, withWidth HOCs
  • StyleRules, Theme types
  • isWidthDown function

css-in-js 👋 

OK, let's try again

New error 😞

  • Quick googling - no results
  • Not-so-quick googling - no results
  • 2 days of investigation

Root cause of the issue is...

React hook call wrapped with if statement

And the solution is

  • Downgrade MaterialUI to latest v3 release
  • Replace webpack with regular TS transpilation

Now it should work!

But it doesn't

Why?

css-in-js 👋 

+

Server Side Rendering


function createStyleContext() {
  return {
    sheetsManager: new Map(),
    sheetsRegistry: new SheetsRegistry(),
    generateClassName: createGenerateClassName()
  };
}

export function getStyleContext() {
  if (!process.browser) {
    return createStyleContext();
  }

  if (!global.__INIT_MATERIAL_UI__) {
    global.__INIT_MATERIAL_UI__ = createStyleContext();
  }

  return global.__INIT_MATERIAL_UI__;
}

Same error appeared in tests


jest.mock('@material-ui/core/styles/createGenerateClassName');
createGenerateClassName.mockImplementation(() => (rule, styleSheet) =>
  `${styleSheet.options.classNamePrefix}-${rule.key}`
);

One more testing issue

Select elements by data-test-id attribute

Naïve approach

  • Specify data-test-id value in components library
  • Use it in library consumers

Such approach won't work in long-term perspective

  • Update value in library
  • Have to update e2e tests in all apps

Better way to handle data-test-id

  • Add data-test-id prop to components' API
  • For complex components, generate data-test-id value

Better way to handle data-test-id

data-test-id + postfix

data-test-id="select"

data-test-id="select-root"

data-test-id="select-option"

Better way to handle data-test-id

data-test-id is omitted from production build

babel-plugin-jsx-remove-data-test-id

Finally!

  • App is running
  • SSR works
  • Styles are not broken
  • Component is on it's place

Conclusion

  • css-in-js - 💩
  • MaterialUI - 💩

Now seriously

  • Easy to work on brand redesign
  • Focus on components and their state
  • Reduced time to interact with design guy
  • Great DX

Library is ready & in use

Benefits we got:

What went wrong

  • Outdated MaterialUI version
  • Quality of tests dropped over time
  • Failed to incapsulate MUI within the library
  • No automatic releases

What's next?

  • Finish redesign
  • Implement more components
  • Update MaterialUI
  • Configure releases

https://www.instagram.com/semin_andrey

Shared Components Library

By Andrey Semin

Shared Components Library

  • 344