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-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
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
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.
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 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
// 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 App
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>
)
}
}
# -- 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
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:
git checkout master
git pull origin master
yarn 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 --tags
cd 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 |