Skip to content

react-component-catalog is a library for registering, retrieving, and rendering React components dynamically based on defined conditions.

License

Notifications You must be signed in to change notification settings

natterstefan/react-component-catalog

Repository files navigation

React-Component-Catalog

npm version Build Status Coverage Status GitHub license

Dependencies Known Vulnerabilities Commitizen friendly lerna

React-Component-Catalog is a library for individually registering, retrieving, and rendering React components based on your own conditions (eg. different component for various clients, sites, ...).

Getting started

npm i react-component-catalog --save

# or
yarn add react-component-catalog

Then install the correct versions of each peerDependency package, which are listed by the command:

npm info "react-component-catalog@latest" peerDependencies

If using npm 5+, use this shortcut:

npx install-peerdeps --dev react-component-catalog

# or
yarn add react-component-catalog -D --peer

Upgrade from 1.x.x to 2.0.0

Catalog Data Structure changes

When upgrading to 2.0.0, one needs to change the Catalog's data structure.

// catalog.js
- import { Catalog } from 'react-component-catalog'
import Button from './button'

-const catalog = new Catalog({
-  components: {
-    Button,
-  },
-})
+const catalog = {
+  Button,
+)

export default catalog

CatalogProvider changes

Previously, CatalogProvider rendered it's children with an empty catalog, when none was provided. In 2.x it renders null instead. Same happens, when no child component is provided.

import { CatalogProvider } from 'react-component-catalog'
import catalog from './catalog' // your apps catalog

const App = () => (
- <CatalogProvider catalog={new Catalog({ components: catalog })}>
+ <CatalogProvider catalog={catalog}>
    <div>Hello</div>
  </CatalogProvider>
)

CatalogProvider accepts an object and no instance of Catalog anymore.

useCatalog and catalog changes

getComponent does not return null anymore when a component is not found, instead it returns undefined.

import React from 'react'
import CatalogComponent, { useCatalog } from 'react-component-catalog'

const App = () => {
- const { catalog } = useCatalog()
+ const catalog = useCatalog()

- console.log('available components', catalog._components)
+ console.log('available components', catalog._catalog)

  const Button = catalog.getComponent('Button')

  // ...
}

Catalog changes

Catalog is not exported anymore, so code like does not work anymore:

- import { Catalog } from 'react-catalog-component'

CatalogComponent and Module Augmentation

The CatalogComponents interface can be augmented to add more typing support.

// react-component-catalog.d.ts
declare module 'react-component-catalog' {
  export interface CatalogComponents {
    Title: React.FunctionComponent<{}>
  }
}

Whenever you use the CatalogComponent now you can do the following to get full typing support (opt-in feature). When you do not provide the interface, any string, string[] or Record<string, any> value for component is allowed.

const App = () => (
  <CatalogComponent<CatalogComponents> component="Title">
    Hello World
  </CatalogComponent>
)

// this works too, but `component` has no typing support
const App = () => (
  <CatalogComponent component="Title">Hello Base</CatalogComponent>
)

Attention: it is recommended to use CatalogComponents only when it was augmented. Because it represents an empty interface and without adding your own custom properties it will match everything.

Basic Usage

Create a Catalog

// button.js
import React from 'react'

const Button = props => <button>{props.children}</button>

export default Button
// catalog.js
import Button from './button'

const catalog = {
  Button,
}

export default catalog

Create a nested Catalog

It is also possible to add a nested components-object to the Catalog. This allows registering variations of a component. Take an article for instance. You might want to register different types of the component. There might be a AudioArticle, VideoArticle and a BaseArticle component you want to use. You can add them to the catalog like this:

// catalog.js
// different types of articles
import AudioArticle from './audio-article'
import BaseArticle from './base-article'
import VideoArticle from './video-article'

const catalog = {
  ArticlePage: {
    AudioArticle,
    BaseArticle,
    VideoArticle,
  },
}

export default catalog

And you could later use it like this:

// app.js
import React from 'react'
import CatalogComponent, { useCatalog } from 'react-component-catalog'

const App = props => {
  const { isAudioArticle, isVideoArticle } = props
  const catalog = useCatalog()

  // get the ArticlePage object from the catalog
  const ArticlePage = catalog.getComponent('ArticlePage')

  // or get them one by one with one of the following methods
  // const BaseArticle = catalog.getComponent('ArticlePage.BaseArticle')
  // <CatalogComponent component="ArticlePage.BaseArticle" />

  if (isAudioArticle) {
    return <ArticlePage.AudioArticle {...props} />
  }

  if (isVideoArticle) {
    return <ArticlePage.VideoArticle {...props} />
  }

  return <ArticlePage.BaseArticle {...props} />
}

export default App

Create a CatalogProvider

// index.js
import React from 'react'
import ReactDOM from 'react-dom'

import { CatalogProvider } from 'react-component-catalog'
import catalog from './catalog'
import App from './app'

ReactDOM.render(
  <CatalogProvider catalog={catalog}>
    <App />
  </CatalogProvider>,
  document.getElementById('_root'),
)

Nesting CatalogProvider

<CatalogProvider /> can be nested, whereas the inner provider will extend and overwrite the parent provider.

// setup catalogs
const catalog = {
  OuterComponent: () => <div>OuterComponent</div>,
  Title: ({ children }) => <h1>OuterTitle - {children}</h1>,
}

const innerCatalog = {
  InnerComponent: () => <div>InnerComponent</div>,
  Title: ({ children }) => <h2>InnerTitle - {children}</h2>, // inner CatalogProvider overwrites Title of the outer catalog
}

// usage
const App = () => (
  <CatalogProvider catalog={catalog}>
    <CatalogProvider catalog={innerCatalog}>
      <Content />
    </CatalogProvider>
  </CatalogProvider>
)

<Content /> can access components inside the catalog and innerCatalog. If the innerCatalog contains a component with the same name than in the catalog it will overwrite it. In this case <Title /> gets overwritten in the inner provider.

Import and use the catalog (with react-hooks)

// app.js
import React from 'react'
// useCatalog is a react-hook
import CatalogComponent, { useCatalog } from 'react-component-catalog'

const App = () => {
  const catalog = useCatalog()
  const Button = catalog.getComponent('Button')

  // you can also first check if it exists
  const hasButton = catalog.hasComponent('Button')

  // or you use them with the <CatalogComponent /> component
  return (
    <div>
      <CatalogComponent component="Title">Hello Client1</CatalogComponent>
      <CatalogComponent
        component="Card"
        {/* the fallbackComponent can either be a new component, or a component
          from the catalog */}
        fallbackComponent={() => <div>Component not found</div>}
        { /* fallbackComponent="FallbackComponent" */ }
      >
        Hello Card
      </CatalogComponent>
      {Button && <Button />}
    </div>
  )
}

export default App

Use catalog with ref

Refs provide a way to access DOM nodes or React elements created in the render method. (Source: reactjs.org)

It is possible to use react-component-catalog with ref as well. It would look similar to (works also with <CatalogComponent />):

const TestComponent = withCatalog(props => (
  <button {...props} type="button">
    Hello Button
  </button>
))

/* eslint-disable react/no-multi-comp */
class App extends React.Component {
  constructor(props) {
    super(props)
    this.setRef = React.createRef()
  }

  render() {
    // or <CatalogComponent component="TestComponent" ref={this.setRef} />
    return (
      <CatalogProvider catalog={{ TestComponent }}>
        <TestComponent ref={this.setRef} />
      </CatalogProvider>
    )
  }
}

How to build and test this package

# -- build the package --
yarn
yarn build
# -- test the package in an example app --
# run the example in watch-mode
yarn watch

# or run the example in production mode
cd packages/example
yarn build
yarn start

How to release and publish the package

This package uses standard-version and commitizen for standardizing commit messages, release tags and the changelog.

When you're ready to release, execute the following commands in the given order:

  1. git checkout master
  2. git pull origin master
  3. yarn release:prepare: select the proper version
  4. yarn release --release-as <version>: use the version selected before (e.g. beta releases: yarn release --prerelease beta --release-as major)
  5. git push --tags
  6. cd packages/react-component-catalog && yarn publish: do not select a new version.

TODO: automate and optimize scripts, see 3ba95ec and 2eb2a8b

Links

Credits

Inspired by Building a Component Registry in React by Erasmo Marín. I did not find a package implementing his thoughts and ideas in such a straightforward way. That is why, I decided to create it.

Licence

Apache 2.0

Maintainers


Stefan Natter