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, ...).
npm i react-component-catalog --save
# or
yarn add react-component-catalogThen install the correct versions of each peerDependency package, which are listed by the command:
npm info "react-component-catalog@latest" peerDependenciesIf using npm 5+, use this shortcut:
npx install-peerdeps --dev react-component-catalog
# or
yarn add react-component-catalog -D --peerWhen 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 catalogPreviously, 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.
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 is not exported anymore, so code like does not work anymore:
- import { Catalog } from 'react-catalog-component'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.
// 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 catalogIt 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 catalogAnd 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// 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'),
)<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.
// 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 AppRefs 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>
)
}
}# -- 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 startThis 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:
git checkout mastergit pull origin masteryarn release:prepare: select the proper versionyarn release --release-as <version>: use the version selected before (e.g. beta releases:yarn release --prerelease beta --release-as major)git push --tagscd packages/react-component-catalog && yarn publish: do not select a new version.
TODO: automate and optimize scripts, see 3ba95ec and 2eb2a8b
- Conventional Commits
- conventional-changelog
- semantic-release (standard-version alternative, with extended CI support)
- commitlint
- npm-dedupe when eg. multiple @types/* versions are installed
- React Type Reference
- Generics while using React.forwardRef
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.
Stefan Natter |